← 全部极客知识库

工程化 / 大模型应用工程化

7. 测试与反馈——怎样证明 AI 改出来的代码真的能用

原文备份 · 第 9 / 21 篇 · 查看极客时间原知识库 ↗

本篇目录 · 9 节

来源:大模型应用工程化

上一课,我们沿着首页到存储方法的调用路径,确认当前搜索没有“先取最近50条再匹配”的问题。可是,假如下一次AI重构查询时不小心引入了这个错误,你靠什么及时发现?

手动搜一次“RAG”,看到结果,并不足以说明旧记录也能被找到。让AI自己再说一遍“逻辑正确”,同样没有增加多少证据。有效的反馈应该能区分两种实现:符合规格的通过,违反关键规则的失败。

本篇会补一个小型边界检查,再让它面对一个故意写错的查询。你将看到4项通过、2项指定失败、恢复4项通过的完整过程。错误只在测试进程中临时启用,不修改应用源码。最终交付是一份能说明输入、预期、实际结果与验证范围的测试报告。

完整练习材料 · 边界检查脚本 · 测试地图 · 报告模板 · 上一篇:上下文工程

核实日期:2026年10月4日。参考检查与教学反例由作者编写,已本地离线执行,不是Kimi生成实录。应用仍使用DeepSeek接口,本课不调用真实模型。Kimi Code仅核对本机2.0.2的help/version与官方文档;没有Windows实机、浏览器视觉或性能压测。

一、独立预期要先于“现在程序返回什么”

测试通常可以拆成三件事:准备已知输入,执行被检查的行为,把实际结果与预期比较。真正困难的往往是第三件事:预期从哪里来?

如果程序当前返回空列表,你就把空列表填进expected,测试当然很容易通过。但这只是记录了现状,未必检验了需求。尤其当AI同时写实现和测试时,它可能在两边重复同一个误解。

本项目已经有现行搜索契约。R5要求:在本会话全部操作中匹配,按时间和ID降序排列,最多显示50条,有更多匹配时提示截断。先不看实现,仅根据这个规则,就能写出以下预期:

已知输入 应显示多少条 truncated应是什么
当前会话没有记录 0 False
恰好50条问题匹配 50 False
51条问题都匹配 50 True
51条记录中只有最旧那条匹配 1,必须是最旧那条 False

第四种情形特别有价值:它会让“先取最近50条再搜索”的错误暴露出来。第二种也不能省略,否则有人把截断条件写成“大于等于50”,可能在恰好50条时误报还有更多结果。

这叫独立预期:从已经确认的规则和可数清的输入推导答案,不从被测函数的输出反推。并不要求每条预期都手写50个ID;按固定编号生成已知顺序也可以,但不能再次调用同一个搜索算法来替它自己出答案。

先打开报告模板,把这四种情形的预期写下来,再阅读参考脚本。这样你是在审查一个答案,而不是先被代码暗示该答什么。

二、测试的名字不决定它实际覆盖哪一层

项目里已经有几组检查,容易产生一个误解:“使用unittest,所以都是单元测试。”unittest是运行和组织测试的框架,测试覆盖到哪里,要看它真正调用了什么。

检查方式 在本项目中做什么 仍然看不到什么
小函数的单元检查 单独检查输入转换、校验等局部逻辑,控制外部依赖 数据库、网页和多步骤连接是否正确
存储层集成检查 调用真实Store,读写临时SQLite,再检查查询结果 q参数怎样映射、页面怎样显示
HTTP层集成检查 经Flask测试客户端走会话、路由、模板和数据库 真实浏览器布局、交互与部署网络
离线流程验收 在临时数据上走预览、演示生成、计划确认等步骤 真实模型质量、线上环境与性能

本篇新增检查属于存储层集成:它不是模拟一个假数据库,而是在临时目录创建真的SQLite文件。因此既能核对返回集合,也能发现一部分查询和数据读取问题。

Flask官方文档说明,测试客户端可以在不启动真实服务的情况下向应用发送请求。本项目原有的--stage web就使用这种方式;它能检查HTTP状态和HTML内容,却不能证明页面在手机上没有遮挡或按钮实际好点。Flask测试文档

分层的作用是帮助定位和控制成本。存储检查通过而网页检查失败,应继续检查路由、参数、会话或模板之间的连接;不能据此直接断定一定是模板错误。少量重要流程再验证部件是否能连接起来。没有必要为了凑齐名词,把一个个人项目拆成几十套测试体系。

三、准备工作区,先运行正常实现

解压到独立目录,进入外层lesson-07。project的38个文件与上一课完全相同,旧验证脚本和57条固定记录也没有修改。新增的是verify_search_boundary.py、任务007、测试地图和报告模板。

先读README与docs/tasks/007-tests.md。上一课任务006的“只调查”属于当时范围,本篇允许在自己的副本中完善新增测试和报告,但不修改应用源码、旧断言或已批准规格。

新增脚本只依赖Python标准库及项目里的存储和校验模块。若只做这组练习,安装Python3.11或以上就够了,不需要API Key。要运行既有HTTP与流程检查,再按照README建立虚拟环境并安装项目依赖。已有合适环境可直接复用。

以下命令统一假定你已按README创建.venv,工作目录为外层lesson-07。

macOS/Linux:

.venv/bin/python verify_search_boundary.py

Windows PowerShell:

.\.venv\Scripts\python.exe verify_search_boundary.py

只使用系统Python时,将上述解释器替换为macOS/Linux的python3或Windows的py -3即可;这一替换仅解决启动方式,运行后面的HTTP检查仍需确保该解释器安装了依赖。

你应看到4个方法通过,最后显示Ran 4 tests与OK,退出码为0。运行时间会因机器变化,不需要和文章一致。每个方法都创建自己的临时数据库,执行结束后清理,不读取个人笔记或已有运行数据库。

“原应用不变”和“新测试通过”是两条不同事实。前者可以用Git差异或文件比较确认;后者必须来自执行,不能根据文件存在就推断。

四、读懂一条断言如何守住边界

打开新增脚本,找到以下方法:

    def test_exactly_fifty_matches(self):
        self.seed(50)
        result = self.store.search_history('A', '普通')
        self.assertEqual([row['id'] for row in result['items']],
                         [f'N{i:02d}' for i in range(50, 0, -1)])
        self.assertFalse(result['truncated'])

seed(50)准备编号N01到N50的50条记录,创建时间依次递增,问题都含“普通”。这些时间和文本由测试直接规定,不依赖当前时钟或真实模型回答。

第二步调用真正的search_history。第三步检查两件事:返回ID是否恰好按N50到N01排列,以及truncated是否为False。仅检查“列表非空”,即使只返回一条也会通过;仅检查长度等于50,则可能漏掉顺序或内容错误。

setUp负责在每项测试前准备临时数据库,addCleanup登记清理动作;assertEqual检查相等,assertFalse检查条件为假。它们是Python标准库的测试接口,无需为本课额外引入框架。Python unittest文档

再看最旧命中那项:51条记录中,只有N01的问题是“旧资料针”,预期ID直接写为['N01']。这份数据很小,却足以区分正确与错误的限量位置。

这里的payload只包含存储查询所需的问题字段,并不是可以送进网页生成流程的完整业务记录。这是有意限定的夹具:测试准备的数据只服务于当前层次。若要验证生成或预览,就应使用对应流程的完整夹具,不能把这个最小结构直接当真实业务样例。

本课补的是0/50/51附近的边界,不覆盖所有维度。它只有一个owner,不能证明跨会话隔离;它也不经过HTML,不能证明输入转义。相关风险继续由原有搜索检查负责。

五、让测试面对一个已知错误

现在启用教学反例。macOS/Linux:

.venv/bin/python verify_search_boundary.py --counterexample

Windows PowerShell:

.\.venv\Scripts\python.exe verify_search_boundary.py --counterexample

脚本会在当前进程中临时把Store.search_history替换成错误方法:先用旧history取最近50条,再匹配关键词。它不会改写project/storage.py,也不会接触你的运行数据库;进程退出后这次替换不再存在。

这次应当失败。作者实际运行得到两个指定失败,以下为关键输出摘录:

FAIL: test_fifty_one_matches
AssertionError: False is not true

FAIL: test_old_match_beyond_recent_fifty
AssertionError: Lists differ: [] != ['N01']

FAILED (failures=2)

第一个失败说明:虽然展示了50条,但错误方法从未看到第51条,因此没能正确标出“还有更多”。第二个说明:最旧的N01在匹配前就被排除了。

空库和恰好50条仍然通过。这个细节很重要:坏实现完全可能通过部分正常用例。如果你只测“能够返回几条结果”,这次重构错误就可能悄悄留下。

如果需要查看退出码,在命令结束后立即输入:macOS/Linux使用echo $?,PowerShell使用$LASTEXITCODE。反例命令应为1;正常命令应为0。不要在中间再执行别的程序后才读取,以免拿到另一条命令的状态。

现在去掉--counterexample,重新运行正常命令,应再次4项通过。无需恢复应用文件,因为它们从未被修改。查看自己的Git差异,确认没有为了演练而改坏源码。

这是一次小型错误注入实验:用已知错误检查断言是否有辨别力。它不保证测试能抓住一切错误,也不是已发现的线上缺陷。日常验收只运行正常模式;不要把预期失败的反例命令混进“必须全部通过”的交付清单。

六、FAIL、ERROR和零测试,代表不同问题

终端出现红字,不一定意味着你成功验证了业务规则。

FAIL通常表示断言比较失败,例如预期N01却得到空列表。ERROR表示执行过程中出现了未按预期处理的异常,例如导入依赖失败、夹具出错。两者都需要查看具体原因,不能把任何非零退出码都当作“测试抓到想要的缺陷”。

本课的反例成功条件很明确:恰好上面两项FAIL,没有ERROR。如果你得到的是ModuleNotFoundError,连目标行为都没检查到。应先修正环境或入口,再观察断言。

还有一种更隐蔽的结果:Ran 0 tests后面也可能出现OK。默认测试发现使用test*.py命名模式,而我们沿用的验证脚本叫verify_...,不能用默认发现命令替代本课明确给出的运行入口。unittest测试发现规则

每次至少检查四件事:运行了哪个脚本,发现多少方法,哪些失败,退出码是什么。对于旧搜索脚本,一个方法中还包含多个subTest案例,方法数不等于业务情形总数;遇到失败时,案例编号比“总共跑了几项”更有定位价值。

把失败反馈交给AI时,也提供这些信息。例如:

在lesson-07执行verify_search_boundary.py --counterexample。
失败项为test_old_match_beyond_recent_fifty:预期['N01'],实际[]。
输入为同一owner的51条固定记录,只有最旧N01含“旧资料针”。
依据R5,不能先取最近50条再匹配。
这是故意启用的教学错误,不是当前应用缺陷,请解释原因,不修改项目。

这比“又报错了,帮我修一下”多提供了足以判断的事实。真实开发时,也应把允许修改的范围和验证方式写清;不要只给最后一行报错就要求助手猜完整背景。

七、用已有检查补齐连接处,而不是重复证明同一件事

边界检查通过后,再了解原来的分层入口。macOS/Linux:

.venv/bin/python verify_search.py project --stage store
.venv/bin/python verify_search.py project --stage web
.venv/bin/python verify_preview.py project
.venv/bin/python project/acceptance.py

Windows PowerShell:

.\.venv\Scripts\python.exe verify_search.py project --stage store
.\.venv\Scripts\python.exe verify_search.py project --stage web
.\.venv\Scripts\python.exe verify_preview.py project
.\.venv\Scripts\python.exe project/acceptance.py

预期依次为2、4、5、10个方法通过。前两组分别在存储与HTTP层使用12个固定子案例。作者本轮已在新工作区实际运行这些入口;没有将上一课结果直接当成本轮输出。

存储层核对长度、大小写、字面匹配、排序等规则;HTTP层还检查重复q返回400、HTML转义、owner来自session、搜索不触发生成,以及原预览链接仍可使用。预览与原流程检查则保护证据统计、确认、防重复与数据恢复等已有行为。

其中用到了mock:在检查期间临时替换某个调用,让意外触发生成时直接报错。它适合隔离不该发生的外部动作,但不能把整个被测搜索函数替换成永远返回正确答案,再宣称搜索通过。教学反例中的替换目的恰恰相反,是主动展示一个确定的错误。

这些局部调用阻断也不是系统级网络沙箱。离线验证成立的依据包括当前代码路径、临时夹具、演示响应和对应阻断;不能推导为任意新代码都绝不可能联网。

以后不必每改一句文档就重复全部回归。改查询逻辑时重点验证结果集合、排序、隔离和网页连接;改模板时检查网页输出与必要手工体验;只改说明时核对事实和链接。选择应由变更影响决定,既不机械全跑,也不因为一项成功就省掉受影响路径。

八、让Kimi参与测试,但保留规则的独立来源

已安装并登录Kimi的读者,可以从外层工作区运行kimi --plan,先让它做一次测试评审。当前官方说明中的Plan模式用于先提出计划再审阅;它不能替你证明断言正确,仍要检查实际动作。Kimi交互与Plan模式

可使用这段任务:

请读取AGENTS、当前任务007、搜索契约R2—R7、测试地图及相关验证脚本。
这次只评审测试,不修改文件、不运行应用或额外外部模型请求。
先从规格推导预期,再指出哪些断言有独立依据、哪些风险仍没覆盖。
重点解释恰好50条与51条、最旧命中、owner隔离和网页转义各由哪里检查。
不要把默认返回值复制成expected,也不要为追求全绿删除断言。
给出最必要的一个补充建议,并说明它针对什么真实遗漏;没有必要就说明原因。

这次开发对话可能使用Kimi额度,读者自行决定是否执行。作者提供的脚本已足够完成离线练习,不要求真实Agent调用。

如果后续确有测试遗漏,让助手只修改已指定的测试文件,再人工审查预期和差异。一个Agent同时写代码和测试并非必然无效,但需要防止两边一起偏离需求。新的测试失败后,先判断实现、预期、夹具或环境哪一方有问题,不能默认“修改测试让它绿”就是修复。

也不必把所有工作都强行套进先写测试的流程。新增可明确描述的行为,很适合先写会失败的验收;探索性界面或简单文案调整,则可能先用可观察结果和人工检查。判断依据是风险与可验证性。

九、交付一份有范围的测试报告

完成后填写docs/TEST_REPORT.md,记录工作副本、真实命令、预期和实际结果、失败证据以及源码是否改变。作者参考材料里的通过结论不等于你自己的运行记录;跳过的步骤就写未运行。

最后回答两个问题:

  1. 边界4项通过,是否证明跨会话数据不会泄漏?不能。本组只有一个owner,必须看相应隔离检查。
  2. 10项离线流程验收通过,是否证明模型回答准确?不能。它使用作者构造响应;其中甚至有一项专门说明机械校验可能放过语义错误。

测试报告最有用的内容,不是“全部正常”四个字,而是哪些行为得到证据支持,哪些尚未检查。本篇完成后,应用功能不变,多了一组能抓住具体错误的边界检查和一份反馈记录。下一篇会进一步处理真实调试与代码审查:怎样从复现信息提出假设、完成最小修复,再审查AI到底改了什么。