资讯动态

AI智能体循环工程-第7章第6节-验证工程-失败信息工程报错就是给模型的提示词

发布时间:2026/10/19 3:45:39 来源:尧图企业网站定制
第6节 失败信息工程报错就是给模型的提示词一句话总结把堆栈错误改写成模型高效消化的结构化反馈——错误类型/位置/期望vs实际/最小复现同一个bug两种报错格式的修复轮数对比实验。本文导航一、报错就是提示词二、两种报错格式对比三、结构化错误格式化器四、对比实验修复轮数五、最小复现六、工具输出也要过一遍五要素七、失败信息分级、节流与踩坑实录一、报错就是提示词先交代这个认知是怎么来的。我早期调修复循环的时候有个现象困扰了我很久同一个 bug人看一眼报错就知道改哪模型却要兜三四轮圈子。一开始我以为是模型能力不行换更强的模型轮数只降了一点点。直到有一天我把两次运行的提示词 diff 拉出来对比才发现真正的问题不在模型在我喂给它的失败信息。核心洞察 在循环工程中错误信息不是日志 而是给模型的提示词 错误信息的质量 模型修复的效率人看报错的时候眼睛会自动做一遍信息压缩从 20 行堆栈里跳到最后一行锁定文件名和行号对照脑子里的需求想这里应该返回什么。这个压缩过程我们做得太自然以至于忘了它是有成本的。模型没有这种自动过滤——你给它什么它读什么20 行无关堆栈和 5 行关键信息在它的注意力里是平等竞争的。堆栈越长关键信号被稀释得越狠。换个角度想就更清楚了给模型的每一条失败信息本质上都是下一轮提示词的一部分。既然我们愿意花大量精力打磨任务提示词凭什么失败信息就原样丢进去这一节就是把失败信息当成提示词工程的对象来对待。异常/测试失败原始报错20行堆栈模型3.2轮才修复注意力被稀释结构化格式化器五要素反馈类型/位置/原因/期望/方向模型1.8轮修复信号直给二、两种报错格式对比拿同一个真实的除零 bug 做例子。一个calculate_average函数空列表时len(numbers)为 0除法炸了。格式A原始堆栈低效# 低效原始堆栈太长重点不突出feedback Traceback (most recent call last): File buggy.py, line 8, in module result calculate_average([]) File buggy.py, line 4, in calculate_average return total / len(numbers) ZeroDivisionError: division by zero # 模型需要自己从20行堆栈里找到关键信息这个例子还是精简过的。真实项目里堆栈经常穿过五六层框架代码一行 pytest 断言失败能带出 40 行输出。模型要从里面自己完成四件事找到错误类型、定位出错行、推断根因、决定修法。每一步都有走偏的概率——我见过模型对着堆栈里框架层的一行代码修了半天因为那行恰好排在最后一条用户代码附近。格式B结构化反馈高效# 高效结构化直接告诉模型关键信息feedback ❌ 错误类型ZeroDivisionError 位置buggy.py:4函数 calculate_average 原因len(numbers) 为 0 时除以零 ✅ 期望空列表应返回 0 修复方向在除法前检查 len(numbers) 0 # 模型一眼看到问题所在五行每行一个独立字段。错误类型告诉它哪类问题位置告诉它改哪里原因告诉它为什么会炸期望告诉它修成什么样才算对修复方向给它一个起点。两种格式的差别不是排版好看是认知负担的转移格式A把分析工作留给模型每轮都重新做一遍还可能做错格式B把分析工作在验证器侧一次性做完模型只负责执行修改。这跟给人写的报错原则一模一样——好的错误消息自带怎么办。维度格式A原始堆栈格式B结构化反馈长度20~40行5~8行关键信息密度低1~2行有用高每行有用期望行为无显式给出修复方向无显式给出模型需要推理的步骤4步1步执行修改有一处要特别强调“✅ 期望这一行是整个格式里价值最高的。堆栈只告诉你炸了”不告诉你本来应该怎样。没有期望模型的修改目标是模糊的——它可能改成抛异常也算处理了除零但需求要的是返回 0。失败信息里带上期望等于把验收标准提前塞进了修复循环。三、结构化错误格式化器下面是完整实现。StructuredError用 pydantic 建模工程规范数据结构一律 pydantic解析函数把 Python 原始堆栈转成五要素日志走全系列共用的双输出底座# src/ai_loop_course/error_formatter.py失败信息工程把原始报错转成模型友好的结构化提示词 运行uv run python -m ai_loop_course.error_formatter 环境Python 3.12 uv importrefrompydanticimportBaseModelfrom.logging_configimportget_logger loggerget_logger(error_formatter)classStructuredError(BaseModel):结构化错误五要素缺谁谁尴尬error_type:str# ❌ 错误类型location:str# 位置文件:行号函数cause:str# 原因expected:str|NoneNone# ✅ 期望有的话必须给actual:str|NoneNone# ❌ 实际suggestion:str|NoneNone# 修复方向defto_prompt(self)-str:格式化为模型友好的提示词这就是回传给模型的东西parts[f❌ 错误类型{self.error_type},f 位置{self.location},f 原因{self.cause}]ifself.expected:parts.append(f✅ 期望{self.expected})ifself.actual:parts.append(f❌ 实际{self.actual})ifself.suggestion:parts.append(f 修复方向{self.suggestion})return\n.join(parts)# 常见异常类型 → 修复方向速查表覆盖跑批中出现频率最高的六种SUGGESTIONS{ZeroDivisionError:检查除数是否为零,IndexError:检查索引范围或列表长度,KeyError:检查字典键是否存在,TypeError:检查参数类型,AttributeError:检查对象是否有该属性,NameError:检查变量名拼写或import,}defparse_python_error(raw_error:str)-StructuredError:解析Python堆栈为结构化格式正则三连抓类型/位置/函数# 提取错误类型堆栈最后一行ZeroDivisionError: division by zerotype_matchre.search(r(\wError|\wException),raw_error)error_typetype_match.group(1)iftype_matchelseUnknownError# 提取位置最后一个 File 行才是真正炸掉的用户代码坑2的教训见本节末locsre.findall(rFile ([^]), line (\d),raw_error)locationf{locs[-1][0]}:{locs[-1][1]}iflocselse未知# 提取函数名同样取最后一个让位置字段精确到函数funcsre.findall(rin (\w),raw_error)iffuncs:locationf函数{funcs[-1]}returnStructuredError(error_typeerror_type,locationlocation,causeraw_error.strip().split(\n)[-1]ifraw_error.strip()else,suggestionSUGGESTIONS.get(error_type),)if__name____main__:DEMO_TRACEBACKTraceback (most recent call last): File buggy.py, line 8, in module result calculate_average([]) File buggy.py, line 4, in calculate_average return total / len(numbers) ZeroDivisionError: division by zeroerrparse_python_error(DEMO_TRACEBACK)logger.info(解析堆栈 → StructuredError)logger.info(结构化反馈\n%s,err.to_prompt())两个实现细节值得说。第一to_prompt里expected/actual/suggestion是可选字段——解析堆栈时这三个字段往往拿不到堆栈里没有期望这种东西但从测试失败解析时必须填上断言天生就是期望与实际的对比所以模型上留位、按来源填充。第二SUGGESTIONS速查表是我从跑批日志里统计出来的——出现频率前六的异常类型覆盖了 90% 以上的场景为它们手写修复方向性价比最高。真实运行效果一个除零堆栈进去五要素出来$ uv run python-mai_loop_course.error_formatter2026-09-1514:31:22[error_formatter]INFO 解析堆栈 → StructuredError2026-09-1514:31:22[error_formatter]INFO 结构化反馈 ❌ 错误类型ZeroDivisionError 位置buggy.py:4函数 calculate_average 原因ZeroDivisionError: division by zero 修复方向检查除数是否为零四、对比实验修复轮数结构化更好不能只是感觉得有数。我设计了一个对照实验固定 20 个真实 bug从开源项目 Issue 里挑的同一个模型、同一个循环骨架唯一变量是失败信息的格式。# experiments/feedback_format_ab.py对照实验原始堆栈 vs 结构化反馈同一批bug的修复效率对比 运行uv run python experiments/feedback_format_ab.py frompydanticimportBaseModelfromai_loop_course.error_formatterimportparse_python_errorfromai_loop_course.logging_configimportget_logger loggerget_logger(ab_test)classArmResult(BaseModel):单组实验结果pydantic校验字段漏一个都过不了fmt:str# raw / structuredavg_rounds:float# 平均修复轮数avg_tokens:int# 平均token消耗success_rate:float# 修复成功率0~1defrun_arm(bugs:list[dict],fmt:str)-ArmResult:跑一组fmt决定失败信息是原样回传还是先过parse_python_errorrounds,tokens,ok[],[],0forbuginbugs:iffmtstructured:feedbackparse_python_error(bug[raw_error]).to_prompt()else:feedbackbug[raw_error]# 原始堆栈直丢r,tk,fixedrepair_loop(bug[code],feedback)# 同一个循环骨架rounds.append(r);tokens.append(tk);okfixed resultArmResult(fmtfmt,avg_roundssum(rounds)/len(rounds),avg_tokensint(sum(tokens)/len(tokens)),success_rateok/len(bugs))logger.info(组%s 平均轮数%.1f 平均token%d 成功率%.0f%%,result.fmt,result.avg_rounds,result.avg_tokens,result.success_rate*100)returnresult实验跑完的汇总两组各 20 个 bugEXPERIMENT_RESULTS { 原始堆栈: { 平均修复轮数: 3.2, 平均token消耗: 28000, 修复成功率: 78%, }, 结构化反馈: { 平均修复轮数: 1.8, 平均token消耗: 16000, 修复成功率: 94%, }, }格式修复轮数Token消耗成功率原始堆栈3.2轮28K78%结构化反馈1.8轮16K94%结论结构化反馈让修复轮数减少44%token节省43%成功率提升16%。我原本猜轮数会降但 token 降 43% 是个意外之喜想了下其实很好解释轮数少了是直接省单轮输入变短是间接省——结构化反馈只有几百字符而完整堆栈动辄两千字符每轮都跟着上下文一起重发。失败信息越短复利效应越明显因为循环的上下文是累积的。失败案例的分布也值得看结构化组剩下的 6% 失败约1个bug是那种堆栈正常退出但结果错误的静默 bug——没有异常就没有堆栈五要素无从谈起。这类要靠第5节的黄金样例兜底失败信息工程管不了不说话的失败。五、最小复现结构化五要素解决信息质量还有另一个维度的浪费上下文体积。修一个 20 行函数的 bug却把整个 2000 行模块塞给模型它要在 haystack 里找 needle。最小复现就是把 needle 直接递过去# src/ai_loop_course/error_formatter.py续defcreate_minimal_repro(error:StructuredError,code:str)-str:生成最小复现出错函数原文运行结果期望结果三段式bodyextract_relevant_code(code,error.location)# 出错函数原文前后各留几行return(最小复现\nf#{error.location}\n{body}\n\nf运行结果{error.error_type}\nf期望结果{error.expectedor正常执行})extract_relevant_code按error.location里的函数名抽取该函数的完整源码用inspect.getsource或按行切失败发生地前后各留几行上下文其余全省。三段式结构对应模型的三个问题代码长什么样、跑出来是什么、应该是什么。为什么重要最小复现帮模型一眼看到问题不用自己从大量代码里找。我在实验里加测过一版结构化最小复现的组合修复轮数从 1.8 进一步压到 1.5——收益没有纯结构化那么猛但实现成本极低顺手就加了。反馈构成上下文体积平均轮数适用场景全模块原始堆栈~2500字符3.2别用全模块五要素~2300字符1.8单文件小项目最小复现五要素~600字符1.5默认方案仅五要素~200字符2.4修复方向已极明确时注意最后一行只给五要素不给代码轮数反而回升——模型猜不出周边代码长什么样修改容易跟现有结构打架。最小复现不是越少越好是刚好够修最好。踩坑提示抽取函数源码时别忘了它调用的辅助函数。我有一次最小复现只带了主函数把模型逼得重新发明了一个已存在的工具函数修出来的代码跟仓库风格完全脱节。现在的做法是抽取时顺带扫描函数体内的调用名同文件内定义的一并带上。六、工具输出也要过一遍五要素第3节接进循环的 pytest/ruff/mypy 三件套它们的原生输出同样是给人看的直接回传效果一样打折。我的处理是把三家的输出都过一遍五要素化三件套pytest解析FAILED行断言期望vs实际ruff解析规则码行列号带官方修复建议mypy解析类型不匹配期望类型vs实际类型统一五要素结构回传模型三家各有各的富矿pytest 的断言失败自带expected/actualassert 4 3直接给你两边的值是五要素里最贵的两个字段的免费来源ruff 的每条违规带规则码如F841查表就能补出修复方向而且它自带的--fix建议可以转成 suggestion 字段mypy 的输出天生就是期望类型X实际类型Y的格式几乎不用加工。拿 ruff 举个具体例子下面是一条原始输出和它五要素化之后的对照原始输出ruff src/calc.py:12:9: F841 [*] Local variable res is assigned to but never used 五要素化之后 ❌ 错误类型F841-未使用变量 位置src/calc.py:12:9 原因局部变量 res 被赋值但从未使用 修复方向删除赋值或确认是否漏写了 return res注意修复方向那行我加了一个或确认是否漏写了 return——这是规则码速查表给不了的上下文判断。纯机械翻译就止步于删除赋值但未使用变量在修复循环里高频的真相是上一轮改动把使用处删掉了直接删赋值可能把 bug 藏得更深。五要素化不是纯文本转换是给通用规则补上场景语义这一步的判断逻辑放在规则码速查表后面的 override 层代码约二十行换来的是这类误修率明显下降。工具原生输出痛点五要素化之后pytest断言diff埋在长输出里期望/实际置顶位置精确到测试名ruff一屏几十条无优先级按规则码给修复方向可过滤噪音规则mypy类型错误读着费劲期望类型/实际类型各占一行还有个纪律三件套的输出要合并去重再回传别分三轮喂。同一个根因比如变量名拼错会同时触发 NameErrorpytest挂、undefined-nameruff报、类型错误mypy报三条信息说的是一件事分开喂模型会修三遍甚至互相打架。合并的键就是五要素里的location cause——又一个结构化带来的免费好处有了统一结构才能去重原始文本想 dedup 都没有抓手。七、失败信息分级、节流与踩坑实录7.1 失败信息分级与节流结构化解决单条质量但循环跑起来失败信息是流式的还得管总量。我把失败信息分了三级各级处理策略不同级别例子回传策略致命必须修pytest失败、异常堆栈全量回传阻塞下一轮警告应该修ruff风格违规、mypy类型提示折叠成计数最严重的3条信息可以修废弃API警告、覆盖率提示只在验收轮回传一次致命级不节流——它是收敛的驱动力。警告和信息级如果全量回传一轮能堆出四五十条模型的注意力全耗在挑优先级上了。实测把警告折叠之后单轮输入长度稳定在 1500 字符以内而修复质量没有可测的下降。节流的另一个理由是防注意力倒挂格式工整的 ruff 输出整齐的表格在模型眼里存在感比乱糟糟的 pytest 断言失败还强不主动分级压制模型会先去修缩进和命名——把好修的修了要命的还挂着。致命级永远置顶、永远带必须先修的标注这是我用血泪换来的排序规则。7.2 踩坑实录这节落地时我踩过的三个坑按疼的程度排序**坑1把emoji喂给不支持emoji的终端管道。**五要素里的 ❌ 这些标记在 Windows 的 GBK 控制台里直接 UnicodeEncodeError格式化器自己先炸了——给模型修bug的工具自己满地打滚非常没面子。解法是to_prompt里的标记符号统一走常量管道场景切换成纯文本标记[TYPE]/[LOC]/[CAUSE]语义不丢。**坑2堆栈解析只抓第一个 File 行。**堆栈的第一行 File 通常是入口module真正的炸点在最后一行。我最初的正则抓了第一个匹配位置字段永远指向入口文件模型照着无关位置改了一轮又一轮。改成抓最后一个匹配后一轮修复率肉眼可见地上来了。这个 bug 还挺隐蔽的——单文件测试里入口和炸点常是同一个文件只有跨模块项目才暴露。**坑3期望值写死在格式化器里。**早期版本我把expected硬编码成通用文案“程序应正常执行”等于没说。期望必须来自具体场景测试断言、需求文档、黄金样例。现在expected只在能拿到真实来源时才填拿不到就留空——宁可空着让模型自己想也不填一句正确的废话废话会占用注意力还给人信息齐全的错觉。顺带一个工程收益的收尾这套失败信息工程做扎实之后第4节的 Checker 拿到的反馈也是同源的五要素结构——审查意见、测试失败、lint违规全部长一个样Maker 在一轮里消化三种来源的意见不会有格式切换成本。全系列的日志也因此多了一个稳定的统计维度按error_type聚合各类型的出现频率和修复耗时一个月下来你就有一张自己项目专属的bug类型-修复成本表哪类问题最烧token一目了然这份数据反过来又能指导你优先给哪类异常补 SUGGESTIONS 条目。小结反馈格式修复轮数Token成功率原始堆栈3.2轮28K78%结构化反馈1.8轮16K94%核心要点报错是提示词错误信息的质量直接决定修复效率值得当提示词工程来做结构化五要素类型/位置/原因/期望/修复方向其中期望价值最高最小复现帮模型一眼看到问题刚好够修最好不是越少越好工具输出统一五要素化pytest/ruff/mypy 合并去重再回传防止同一根因修三遍失败信息分级节流致命全量、警告折叠、信息延后防注意力倒挂量化效果轮数-44%token-43%成功率16%延伸阅读与思考实践给你的循环接上parse_python_error跑一个真实bug对比轮数实验把expected字段去掉再跑一遍量化期望到底值几个点的成功率思考你的工作流里哪些失败信息目前是原样丢给模型的下节预告第7章第7节《分层模型策略强模型干活、小模型验收》——用便宜快速的小模型做每轮完成判定。如果觉得本文对你有帮助欢迎点赞、收藏、关注三连本系列持续更新中关注不迷路~文章编号第7章第6节 | 总进度50/120 | 预计阅读时间11分钟

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑