AI时代软件工程:从辅助编码到确定性系统的架构与测试之道
今年我完整跟完一门编号为CS146S的课程主题就是“AI时代的软件工程”。本来我以为这又是一门教你怎么用AI写代码的工具课结果越听越发现它真正在讨论的是另一个问题当代码的生产方式从“人写”变成“人审、AI写”之后软件工程那些老规矩还有哪些仍然成立。这门课给了我一个很明确的答案——绝大部分老规矩依然成立只是执行顺序和关注重心变了。这门课适合谁我觉得有三类人收获最大正在用AI辅助开发但总觉得不可控的工程师负责AI产品落地但说不清怎么保障质量的技术管理者还有打算系统梳理AI开发流程、不想只停留在“会调API”层面的学习者。它不是教你某个具体模型怎么用而是教你一套在AI语境下依然能打的工程方法论。1. 这门课到底在讨论什么AI没有推翻软件工程只是重排了优先级课程第一周就抛出一个很扎心的问题如果一个AI能一天生成几千行代码那还需要软件工程吗听起来好像不需要但我身边真实发生过的场景是团队用AI快速拼出一个系统结果没人说得清楚它为什么这么写、改哪条逻辑会炸、怎么证明它是对的。这恰恰是软件工程存在的理由。所以这门课的核心立场很鲜明AI不会让软件工程消失只会让软件工程的重心从“关注怎么写代码”转向“关注怎么证明代码是对的、怎么控制不确定性”。1.1 传统软件工程的核心问题在AI时代原封不动很多人以为AI能把需求模糊、复杂度失控、质量保障这些老问题一并解决事实恰好相反。需求不确定性反而更严重了。以前用户说“做一个订单列表”你还能掰扯清楚字段和状态现在用户说“做一个智能助手”边界在哪里完全靠猜。AI的能力范围是个开放集需求方很容易把期望抬高到“它什么都能干”而工程方必须把它拉回“它只能在这些场景下可靠工作”。这是最花时间、也最容易被低估的一环。复杂度管理也一点没减少。AI生成的代码越多系统里互相牵连的隐性依赖就越多。代码量不是衡量复杂度的标准调用关系、数据流、外部依赖才是。模块划分、接口设计、分层隔离这些基本功在AI写的代码堆里反而更重要因为AI不会替你承担架构责任。质量保障更是如此。以前测试是在验证“实现是否符合规格”现在测试还要多问一句“这个输出是否在可接受范围内”。传统测试的确定性断言依然需要但远远不够。质量的定义变宽了质量保障的手段必须跟着扩展。协作与沟通的老问题也在换皮重现。AI生成代码之后代码评审不再是“看一个人写得对不对”而是“看AI写的这段合不合业务预期、有没有埋雷”。文档、设计评审、变更管理这些流程不是被简化了而是被重新发明了一遍。1.2 变的不是目标是“确定性”这个前提为什么AI会让软件工程产生这么多连锁反应根源在于一个前提变了传统程序是确定性的同样的输入必然得到同样的输出而AI模型是概率性的同样的输入可能给出不同的结果。这个变化看起来不大影响却是全局性的。维度传统软件AI赋能的软件输出行为确定性可精确断言概率性只能约束范围调试方式断点定位、日志追踪提示词排查、数据排查、上下文排查测试重点功能逻辑与分支覆盖输出质量、边界治理、稳定性异常处理明确错误码与异常类型超时、重试、降级、兜底策略回归保障自动化测试用例评测集自动化用例双轨这个表格基本就是整门课的地图。后面聊的测试、架构、评审都是在围绕“概率性输出如何进入确定性系统”展开。1.3 我理解的全景图把课程主线压缩一下其实就四条AI辅助开发怎么用才靠谱、AI应用的测试与质量怎么保障、AI原生系统怎么设计架构、AI进入团队后协作流程怎么改。这四条也是我下面要展开的内容。2. AI辅助开发的正确打开方式别把大模型当成编译器课程里有一句话我记了很久把AI当成编译器你会收获一个满嘴胡话的编译器把AI当成一个能力很强但经常犯迷糊的新同事你才知道怎么跟它配合。这句话太真实了。我用AI辅助编程有一段时间了最大的感受是它的产出质量和你的使用方式强相关。不是“问得越多越好”而是“问得越结构化越好”。2.1 我在实践中常用到的三层用法第一层是局部代码生成。处理样板代码、格式化逻辑、简单工具函数这种低风险任务AI基本稳得可怕。比如写一个日期范围拆分的函数、把一段混乱的配置整理成结构化的对象这类需求边界清晰、验证成本低直接交给AI生成能省不少时间。第二层是代码理解与重构。我经常拿一段自己不熟悉的代码问AI“这段逻辑的入口在哪里数据流是什么有没有可以拆分的点”这种用法下AI更像一个可以随时对话的代码阅读助手。注意它给出的理解结论不一定全对但能帮我快速建立初步认知再用调试工具验证一遍效率比从头啃高很多。第三层是测试与文档生成。让AI根据函数签名和业务描述生成单元测试骨架、生成接口文档、生成变更说明这些都是AI非常擅长的。但我不建议让AI在没有人review的情况下直接产出业务级测试断言原因后面专门说。每层用法都有一个共同前提你要有能力判断AI输出对不对。这不是劝退而是提醒。如果你自己完全看不懂一段代码AI生成的代码对你就是纯风险。2.2 提示工程在软件工程里不是聊天技巧而是需求规格很多人把写提示词当成聊天技巧其实在工程场景里提示词就是一份需求规格说明书。你在提示词里写清楚了角色、上下文、输出格式AI的行为才会收敛到你预期的范围内。我自己的标准模板是三段式角色定义、任务上下文、输出约束。举个例子我经常用AI做代码评审预览提示词是这样写的请以一名资深代码评审员的身份审查下面的代码片段。 按【正确性】【安全性】【可维护性】【性能】四个维度输出问题清单。 每个问题必须包含严重级别高/中/低、问题定位文件行号或函数名、修复建议。 不要输出任何赞美性语言只输出问题和建议。 没有问题时直接输出“未发现明显问题”。 代码片段 在这里粘贴代码为什么这么写角色定义让AI调用专业领域的知识框架任务上下文让它知道审查范围输出约束让结果可以直接进追踪系统。对比一下如果你只是说“帮我看看这段代码”它很可能给你一堆正确的废话你还要自己再筛一遍。2.3 涉及大段代码时怎么管理上下文这是AI辅助开发里最容易被忽视的坑上下文不是越长越好。模型对过长的上下文会产生注意力衰减也就是“中间段信息被忽略”尤其是当代码量大、逻辑复杂的时候。我的做法是分块提交。先把文件结构梳理清楚再把有疑问的模块、函数单独摘出来提问。对需要全局信息的问题先让AI分别处理几个局部再让它做汇总推理。比如“分析这个服务的调用链”我不会把整个仓库丢进去而是先让它分别梳理入口、业务层、数据层的关键路径最后再合并。还有一个小技巧在提示词里明确引用依赖关系。只贴代码片段而不说明它依赖什么库、什么接口AI生成的答案就可能基于错误假设。给它一行说明“这段代码依赖某个模块的某某函数请结合这个约束分析”准确率会明显提升。3. AI时代的测试策略从人工断言到“用AI测试AI”课程里有一句贯穿始终的话AI应用测试的难点不是覆盖代码行而是覆盖“输出的合理程度”。传统测试用例是“跟预期结果比”AI测试用例是“跟用户可接受的范围比”。这句话让我想通了很多以前模糊的问题。AI应用不是不需要测试而是测试要做的事情更多、更难了。3.1 传统测试的基本盘不能丢先泼一盆冷水AI写得再快单元测试、集成测试、回归测试这些基本盘依然不能丢。理由很简单——AI生成代码只是改变代码的生产方式没有改变代码要正确运行的底线。我见过很多团队用AI生成了一堆代码然后测试覆盖率接近零。原因是“AI生成的代码还要测什么”结果一上线就出问题。AI生成的代码尤其需要测试因为你不知道它是基于什么假设写出来的。单元测试是对AI输出的一种实时校验也是后续重构的安全网。我的建议是无论代码是不是AI写的测试策略严格按照工程标准来。每个核心函数要有单元测试模块间要有集成测试主流程要有端到端测试。AI可以帮你生成测试骨架但测试的标准必须由人来定。3.2 AI生成测试用例能做什么不能做什么AI生成测试用例的能力已经不错了但边界要搞清楚。能做的事情包括根据代码结构自动生成覆盖分支的用例、生成各种mock数据、构造边界输入空值、超长字符串、负数字段。这些是模式匹配任务AI做得又快又好。不能做的事情更关键。AI不知道你的业务预期是什么它判断不了“这个输入下正确结果应该是多少”。让它生成测试用例经常会出现断言值是从代码实现里反向抄过来的情况也就是“用实现验证实现”这种测试等于没写。另外数据隐私、合规边界这些业务规则AI也无法替你把关。所以AI生成的测试用例必须经过人工review重点就是要看断言是不是符合业务预期而不是只看有没有跑通。3.3 面向LLM应用测试要新增四个维度如果系统里接入了大模型传统的测试维度是不够的。我在这门课里学到并验证过的四个新维度值得直接抄进自己的测试计划。第一是输出结构校验。模型输出经常“有噪音”该是JSON的时候给你夹带解释文字该是枚举值的时候给你自由发挥。输出结构不校验下游解析迟早翻车。这个维度在集成测试里就跑不用等线上。第二是幻觉与事实性检测。特别是生成摘要、抽取信息的场景要检查输出里是否出现了输入材料中完全没有的事实。简单做法是抽几个关键实体做比对比如生成内容里提到的人名、机构名、时间点去原文里找对应。第三是稳定性测试。同一个输入问三次输出了三个不同答案这在聊天场景也许还能忍但放在金融、客服、审批场景就是事故。稳定性测试要设置可接受的波动范围差的场景必须收敛。第四是成本与性能阈值。LLM应用的响应时间和token消耗是动态的测试用例里应该包含“这个请求不能超过多少token、多少毫秒”的硬性指标。成本失控往往不是某一次调用贵而是没有为单次调用设上限数量一起来就爆了。3.4 一个简单的回归评测集思路换提示词、换模型、调参数都可能让效果变好同时让另一个场景变差。我习惯把关键输入输出沉淀成一个评测集每次改动后跑一遍。EVAL_CASES [ { prompt: 请总结这段新闻..., must_contain: [关键人名, 核心事件], forbidden: [虚构机构, 编造时间] }, { prompt: 请用中文回答..., must_be_language: zh, max_tokens: 100 }, ] def run_eval(call_model, cases): failures [] for case in cases: output call_model(case[prompt]) for keyword in case.get(must_contain, []): if keyword not in output: failures.append(f缺少关键信息: {keyword}) for banned in case.get(forbidden, []): if banned in output: failures.append(f出现不应有的内容: {banned}) if len(output) case.get(max_tokens, 1000): failures.append(输出超长) return failures这个脚本很简陋但理念是对的把“输出质量”变成可以度量的任务。评测集不怕简单关键是要持续积累。我每次处理用户反馈、线上问题都会顺手把案例补进评测集时间长了这就是你团队真正的质量资产。4. AI原生系统的架构设计把不确定性关进笼子里“AI原生”这个词现在有点被用滥了。在我看来真正的AI原生不是“调用了大模型API就算数”而是整个系统架构从设计第一天就接受了“模型会犯错、会超时、会换版本、会行为漂移”这个前提。这门课花了不少篇幅讲架构核心就一句话把不确定性关进笼子里别让它顺着代码流得到处都是。4.1 调用层要做防腐设计很多团队在一开始图方便让业务代码直接调用某个模型供应商的SDK。等哪天要换模型、换供应商或者对方改了一个参数语义你就得满项目去找调用点改代码。正确做法是加一层防腐层把模型调用封装成一个内部接口。业务代码只认识这个接口不关心背后用的是哪家模型。class AIModelClient: def __init__(self, provider, primary_model, fallback_modelNone): self.provider provider self.primary_model primary_model self.fallback_model fallback_model def generate(self, prompt, temperature0.2, max_tokens500): try: return self._call_primary(prompt, temperature, max_tokens) except TimeoutError: if self.fallback_model: return self._call_fallback(prompt, temperature, max_tokens) raise except RateLimitError: # 可以考虑加退避重试或降级到缓存结果 return self._fallback_with_cache(prompt)这一段代码的核心作用有两个一是隔离变化换模型、加参数都不影响业务层二是统一处理异常把超时、限流、无结果都收敛到一个地方。没有这层壳AI应用就像没装保险丝的电箱小故障也能烧掉整片电路。4.2 上下文工程Context才是AI应用的数据库课程里有个观点我特别认同对AI应用来说上下文就是数据库。模型的输出质量很大程度上取决于你给它喂了什么样的上下文。但上下文不是越多越好。很多团队为了“让模型更懂业务”把超长文档、全部历史对话一股脑塞进prompt结果模型反而抓不住重点成本还飙高。我对上下文工程的理解是三个动作检索而不是全量塞入。需要知识库的时候用检索拿到最相关的几段内容而不是把整个知识库塞进去。这就像给人查资料你不会扔给他一整座图书馆而是给他翻到正确页码的几页纸。摘要而不是保留全文。长对话场景下历史消息超过一定轮数后把之前的对话总结成结构化摘要保留关键信息丢掉冗余。分层而不是平铺。上下文按“系统指令用户意图参考材料工具结果”四块组织让模型知道哪部分是必须遵守的规则哪部分是可参考的资料哪部分是当前要处理的任务。结构化上下文比一大段散文式的描述靠谱得多。4.3 可观测性与成本控制AI系统的排障思路和传统系统很不一样。传统系统可以依赖日志和断点复现问题AI系统的问题往往是“同样的输入结果时好时坏”这时候没有记录就别想排查。我的做法是给每一次模型调用记录四件事模型名称和参数、输入token和输出token、响应耗时、返回结果摘要。日志里有了这些用户反馈一个问题时你才能回答出“当时模型收到的是什么、输出的是什么、花了多少钱”。成本控制也有一个实用策略路由分层。不是所有请求都需要用最强的大模型简单的文本分类、关键词抽取、格式整理用小模型处理又快又便宜复杂的推理、长文总结、代码生成再调大模型。再配合缓存层复用重复请求总体成本能降下来不少。我遇到过一次线上事故就是因为某次调整无意中把一大段历史信息加进了prompt导致token消耗翻倍、响应延迟明显变高。没有日志记录这个问题排查起来会非常费劲。这也是我坚持“一切调用皆可观测”的原因。5. 团队协作与代码评审AI产物的“人机共同评审”AI进入团队之后最容易翻车的环节是代码评审。因为AI生成代码有一种很强的说服力格式干净、注释齐整、命名规范看上去非常专业。但逻辑漏洞、隐藏依赖、安全风险恰恰藏在漂亮的外壳下面。这周课上讨论的很多案例都指向同一个结论AI时代评审不是变轻松了而是变重了。以前评审是人和代码之间的事现在变成了人和AI之间的博弈。5.1 人审什么四个不嫌多的检查点我给自己总结了一套固定的AI代码审查清单不管AI生成什么代码都按这四关过一遍。第一关是正确性。看边界条件是否处理了空值、重复值、异常输入下会不会崩。AI经常在“看起来正常”的路径上表现良好但在边界状况下露出破绽。第二关是安全性。权限校验写没写敏感数据有没有泄漏到日志或模型输出那些看起来无害的AI生成代码可能恰好绕过了原本的安全控制。第三关是合规性。引用的开源代码许可证是否允许我们这么用模型输出的内容是否涉及数据权限AI不会主动告诉你“这部分代码可能来自某个你没有权的来源”这个只能人来确认。第四关是可维护性。这段代码是不是重复造轮子抽象得是否过度有没有引入不必要的复杂依赖AI容易生成表面统一、实际孤岛式的重复代码时间长了就是技术债的温床。5.2 把AI接入现有研发流程的正确姿势AI不该替代流程而是嵌入流程。我比较认可的几种姿势是在代码评审前用AI做一个变更摘要。特别是大范围的改动AI可以自动生成“这个PR改了什么、影响哪些模块、可能的引入风险”评审人先看摘要再深入效率高很多。在CI流水线里加一道AI静态检查。跑的维度就是刚才说的正确性、安全性、可维护性但注意AI检查的结果是“建议”不是“结论”。所有告警还是要有人确认后再处理。在提交环节用AI生成变更说明和文档补充。这个收益是最直接的Commit Message和接口文档能跟上代码变化是小团队最容易欠的债。一定要记住AI在任何流程里都只做“预审”和“建议”最终拍板的必须是负责人。如果有人把AI检查通过当成“可以合并”的唯一标准这个流程迟早出事。5.3 我踩过的三个坑挑三个我真实遇到过的问题分享一下给各位当个参考。坑一AI生成了一段看起来完全正确的匹配逻辑根据某个字段做优先级排序。常规数据下跑得好好的结果线上出现极端数据时排序结果完全错乱。教训是AI写的逻辑边界条件一定得自己补测。坑二AI自动生成了大量重复代码。效率确实高但这个模块修了bug另一个模块没修排查的时候才发现有十几个几乎一样的实现。教训是AI帮你可以但架构规范不能放松重复逻辑要持续收敛。坑三AI生成代码时引用了一个未声明的依赖库本地环境碰巧有打包环境直接构建失败。教训是AI生成的依赖声明一定要检查别默认它写的都对。6. 常见问题与排查技巧实录最近半年我在不同项目里反复遇到的问题基本集中在下面几类。整理成一张速查表也把排查思路写清楚了。症状/场景可能原因推荐处理方式相同问题多次调用结果差异很大采样参数设置偏高把temperature调低优先用0或0.1对输出格式用结构化约束提示词一长效果就明显变差上下文超出模型有效注意力范围引入检索只保留相关片段长对话做摘要压缩token成本快速增长大量重复请求未做缓存加缓存层批量合并请求小模型处理简单任务AI生成的代码评审时没问题上线后才出错评审缺少业务上下文人工用业务用例走查让AI额外列出它注意到的边界条件换了模型版本后输出风格突变没有沉淀评测集建立回归评测集升级前全量跑再决定是否上线模型频繁超时或限流单一调用链过重没有降级方案增加超时控制、重试退避、备用模型降级排查这类问题有个通用逻辑先看记录再看上下文最后动参数。没有记录就去改参数等于闭着眼睛调琴弦调到哪里算哪里。还有一个容易被忽视的地方很多AI应用的“不稳定”其实不是模型的问题而是输入上下文变化造成的。你以为两次输入一样但上下文里可能藏着上次对话留下的信息或者知识库检索回来不同的片段。排查时先固定上下文再对比结果很多神秘问题当场就清楚了。最后聊一点个人体会。整个课程下来我最认同的一句话是AI时代的软件工程师核心竞争力从“写得快”变成了“判断得准”。这件事听起来抽象做起来很具体——多建评测集多写测试多维护日志多人工走查边界。上完这门课之后我给自己定了一个小习惯每天把AI帮我写的代码和它没说清楚的地方记录一遍慢慢就会很清楚哪些场景适合交出去哪些场景必须人工兜底。这个习惯比任何工具都管用。