核心摘要
大模型自纠错的价值取决于反馈能否指出真实缺陷,以及后续检查能否确认修复。模型说“已经改好了”不能替代验收。工程上应分离候选生成与接受逻辑,保留已评估版本、拒绝回归,并同时统计修对了多少错误、改坏了多少正确答案,判断实际收益。
目录
自纠错能说明什么
自纠错是针对已有答案执行反馈与修订的过程,结果可能变好、不变,也可能变差。把流程命名为“纠错”,并不等于它已经成功。
在应用中,应把四件事分开:
- **发现错误:**证明某项要求没有满足。
- **定位问题:**指出相关字段、结论或行为。
- **提出修复:**生成针对问题的修改。
- **验收候选:**按完整契约重新检查修改后的结果。
JSON 解析器能发现格式错误,却不能证明发票金额与原始记录一致。失败测试能指出行为不符,却未必定位根因。流畅的评论可能两者都做不到。不要把这些不同能力压缩成一个由模型自行给出的置信度。
推理模型 可以在单条轨迹中表现出重新考虑的行为;应用也可以围绕普通 大语言模型 编排修订。两种方式都不要求向用户暴露完整 思维链,应以可观察的输出和检查结果评估。
“选择”是另一个操作:Best-of-N 在完整候选中选择,束搜索不断扩展和裁剪局部候选,序列修订修改已有候选。因此,生成多个答案再排序,不能直接等同于自纠错或束搜索。
现有研究支持哪些结论
已有研究提出了多种反馈机制,也报告了明确的失败情况。它们不能证明每轮修订都有固定收益,更不能把整张模型榜单的分数归因于自纠错。
| 研究 | 核心机制 | 对工程的启发 |
|---|---|---|
| Self-Refine | 模型生成、评论并修订输出,不额外训练权重 | 可以围绕已有模型构建反馈流程 |
| Reflexion | 把语言反馈放入情景式 Agent 记忆,影响后续尝试 | 保存有用的失败证据,避免盲目重试 |
| CRITIC | 通过工具交互获得反馈并修订 | 外部证据可以帮助挑战模型错误 |
| Large Language Models Cannot Self-Correct Reasoning Yet | 研究没有外部反馈的内在纠错 | 部分受测设置不改善,甚至退化 |
Self-Refine 在其任务上的整体提升,不代表每次修订都有收益。Reflexion 同时允许外部反馈和内部模拟反馈,把它定义成“只靠外部反馈”会遗漏方法的一部分。其基准结果来自论文中的 Agent 协议,不能当成任意包装器或原始单次模型调用的分数。
内在纠错研究的结论受当时受测模型和实验条件限制。它反驳的是“默认自纠错有效”的假设,不是宣判所有未来系统都无法纠错。
DeepSeek-R1 报告 区分了不经过初步监督微调、直接开展强化学习的 R1-Zero,以及包含冷启动数据和多阶段训练的完整 R1。因此,不能把 R1 的所有能力都归因于同一种“纯 RL”设置。可见推理文本也不能证明模型内部采用某种特定验证器,更不能单独解释纠错对基准成绩的贡献。
OpenAI 推理指南 说明 API 层面的控制参数与 Token 核算,并没有提供推断内部搜索树结构或逐轮纠错行为的依据。应用自己的机制应讲清楚,服务商没有公开的内部实现应保持未知。
怎样设计有效反馈
有效反馈需要说明失败的要求、观察到的证据,以及修改时必须保持的条件。“请更准确一点”不足以指导可靠修复。
例如,发票摘要的反馈可以是:“原始记录中,小计为 10,000 分,税额为 800 分,合计应为 10,800 分;当前输出 10,700 分不正确。币种必须保持 CNY。”原始金额属于任务契约;如果允许模型修改小计来凑出自己的合计,验证就失去了意义。
| 任务 | 有帮助的反馈 | 不充分的替代品 |
|---|---|---|
| 结构化提取 | 来源片段与字段不一致之处 | 仅通过 Schema |
| 代码生成 | 失败输入、预期行为、实际行为 | “这里可能有 bug” |
| 有依据的问答 | 缺乏支持的结论与相关原文 | 另一段流畅回答 |
| 任务规划 | 违反容量或权限的具体约束 | 更高的自报置信度 |
还应把硬约束与偏好分开。币种正确、目标已授权、必填字段存在,不能被更高的文风分数抵消。模型评审可以辅助判断可读性,但不能覆盖未通过的强制检查。
反馈应绑定候选版本、任务版本、验证器版本和来源快照。任意一项变化后,沿用旧验证结果都可能造成误判。文本差异有助于检查改动,但变化量不是正确性指标:一个负号足以改坏答案,大幅重写也可能真正修复问题。
用 Go 实现带回归检查的修订循环
下面的 Go 1.23 示例为发票摘要实现保守的验收策略:保留最佳已评估候选,不允许丢失已有通过项,并且每个修订使用前都重新检查。修订函数返回预设候选,仅用于复现控制逻辑,不调用真实模型,也不测量模型效果。
币种、小计和税额来自可信输入;生成结果只包含币种和合计。金额使用整数最小货币单位,避免浮点舍入,并拒绝非法或会溢出的来源金额。
保存为 main.go 后执行 go run main.go。
package main
import (
"context"
"fmt"
"math"
"time"
)
type Source struct {
Currency string
Subtotal int64
Tax int64
}
type Draft struct {
Currency string
Total int64
}
type Check struct {
Name string
Pass bool
}
type Outcome struct {
Draft Draft
Checks []Check
Accepted bool
Revisions int
Rejected int
Stop string
}
type Repair func(context.Context, Draft, []string) (Draft, error)
func assess(ctx context.Context, source Source, draft Draft) ([]Check, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if source.Currency == "" || source.Subtotal < 0 || source.Tax < 0 ||
source.Subtotal > math.MaxInt64-source.Tax {
return nil, fmt.Errorf("invalid source")
}
return []Check{
{"currency_matches_source", draft.Currency == source.Currency},
{"total_nonnegative", draft.Total >= 0},
{"total_matches_source", draft.Total == source.Subtotal+source.Tax},
}, nil
}
func failures(checks []Check) []string {
var names []string
for _, check := range checks {
if !check.Pass {
names = append(names, check.Name)
}
}
return names
}
func improves(previous, next []Check) bool {
if len(previous) != len(next) {
return false
}
gained := false
for i, check := range previous {
if check.Name != next[i].Name || check.Pass && !next[i].Pass {
return false
}
gained = gained || !check.Pass && next[i].Pass
}
return gained
}
func revise(ctx context.Context, source Source, initial Draft, limit int, repair Repair) (Outcome, error) {
result := Outcome{Draft: initial}
if limit < 0 || repair == nil {
return result, fmt.Errorf("invalid configuration")
}
checks, err := assess(ctx, source, initial)
if err != nil {
return result, err
}
result.Checks = checks
seen := map[Draft]bool{initial: true}
for {
if err := ctx.Err(); err != nil {
return result, err
}
failed := failures(result.Checks)
if len(failed) == 0 {
result.Accepted, result.Stop = true, "checks_passed"
return result, nil
}
if result.Revisions == limit {
result.Stop = "revision_limit"
return result, nil
}
result.Revisions++
next, err := repair(ctx, result.Draft, failed)
if err != nil {
return result, fmt.Errorf("repair failed: %w", err)
}
nextChecks, err := assess(ctx, source, next)
if err != nil {
return result, err
}
if seen[next] {
result.Stop = "repeated_candidate"
return result, nil
}
seen[next] = true
if !improves(result.Checks, nextChecks) {
result.Rejected++
continue
}
result.Draft, result.Checks = next, nextChecks
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
source := Source{"CNY", 10000, 800}
proposals := []Draft{{"USD", 10800}, {"CNY", 10800}}
index := 0
repair := func(ctx context.Context, current Draft, failed []string) (Draft, error) {
if err := ctx.Err(); err != nil {
return Draft{}, err
}
if len(failed) == 0 || index >= len(proposals) {
return Draft{}, fmt.Errorf("no scripted repair")
}
next := proposals[index]
index++
return next, nil
}
result, err := revise(ctx, source, Draft{"CNY", 10700}, 2, repair)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Printf("accepted=%t revisions=%d rejected=%d currency=%s total=%d stop=%s\n",
result.Accepted, result.Revisions, result.Rejected,
result.Draft.Currency, result.Draft.Total, result.Stop)
}
预期输出:
accepted=true revisions=2 rejected=1 currency=CNY total=10800 stop=checks_passed
第一次修订把金额改对,却把币种换成了 USD,因此被拒绝。第二次修订全部通过;即使它已经是最后一次允许的修订,也必须在评估完成后才能接受。
如果上限只有一次,返回的仍是 CNY 10,700,Accepted=false,停止原因为 revision_limit。这是保留的已评估草稿,不是可批准的发票。上限为零时仍检查初始候选;重复候选会停止循环;修订出错时返回错误,并保持保留候选与其证据对应。
这套严格策略可能拒绝大范围重写需要经过的中间状态。如果任务确实需要探索,应单独维护实验分支,同时保留最后可接受的结果,不能为了继续搜索而降低强制验收要求。
真实接入时,将预设修订函数替换为模型调用,并传入固定任务事实与具体失败项。解析返回值为 Draft,检查响应是否完整,同时预留生成和验证预算。回调必须遵守 context;Go 控制器的取消信号不能强制中断任意阻塞函数。
分别评测修对与改坏
自纠错最重要的指标,是同一批任务前后正确性的变化,而且评测标签应独立于修订循环。
对于固定的 N 个任务:
- **修对数量:**初始错误、最终正确。
- **改坏数量:**初始正确、最终错误。
- 准确率净变化:
(修对数量 - 改坏数量)/ N。 - **修对率:**修对数量除以初始错误任务数。
- **改坏率:**改坏数量除以初始正确任务数。
举个纯粹用于演算的例子:100 个答案中,最初 60 个正确。修订把 40 个错误中的 12 个修对,又把 60 个正确答案中的 5 个改错。最终准确率是 67%,净增 7 个百分点;修对率为 30%,改坏率约为 8.3%。这些数字不是模型实测结果。
只报告“修复了 12 个答案”会隐藏五次回归;只报告最终准确率,又无法解释流程究竟在哪里改善或失败。
要判断反馈是否有价值,还需要区分“多做一次计算”和“获得了有效证据”:
| 对照方案 | 检验什么 |
|---|---|
| 只返回初始答案 | 起始质量 |
| 相近预算下重新生成 | 多一次尝试的价值 |
| 没有具体证据的修订 | 泛化自我评论的效果 |
| 带具体反馈的修订 | 错误证据的额外价值 |
| 完整验收控制器 | 能否保留改善并拒绝回归 |
同时记录最终答案质量、回答覆盖率、错误接受、错误拒绝、延迟与总成本。弃答要单独报告,不能悄悄从分母中删除。每轮保留候选 ID 和检查证据,使最终结果能够对应到真正评估过的版本。
修订期间可见的开发测试,应与留出的评测测试分开。反复根据同一套测试修代码,可能只是过拟合测试集。换一个评审模型或 temperature 可以增加判断多样性,但不能证明判断统计独立,也不能消除共同盲区。
控制成本与执行风险
有边界的纠错流程既需要资源限制,也需要限定反馈能授权什么。更多轮次增加修复机会,也增加引入错误的机会。
初始生成、评论、修订、工具执行和最终验证都应纳入预算。在最后一次生成前预留验证额度,避免刚好在准备返回结果时已经无力检查。推理时计算指南 进一步说明共享额度预留与用量对账。
对 AI Agent,应区分修改产物与执行外部操作。改写草稿和再次提交订单不是一回事;如果调用超时但可能已经提交成功,应先核对实际结果,再决定能否重试。
工具输出、检索文本是证据,不是扩大权限的授权。失败测试不能授权读取密钥。生成代码应放在隔离执行环境中运行,带超时的子进程本身不构成沙箱。
Reflexion 式记忆也需要作用范围。应保存具体失败、支持证据及适用条件;如果把错误反思复制到所有任务,会使失败持续出现。按任务或租户隔离,必要时设置有效期,并在验证器或来源变化后撤销失效经验。
常见问题
什么是大模型自纠错?
自纠错是模型根据错误反馈修改先前答案的过程,可以发生在一条推理轨迹内部,也可以由应用编排生成、检查和修订。只有后续证据表明答案变好,才能认定纠错成功。
Self-Refine 和 Reflexion 有什么区别?
Self-Refine 用模型完成生成、评论和修订,不需要额外训练权重。Reflexion 把语言反馈存入情景记忆,用于后续尝试;反馈既可以来自外部环境,也可以由内部模拟产生。
为什么自纠错会越改越差?
模型可能接受误导性反馈,或者把原来正确的答案改错。应分别测量错误被修对与正确答案被改坏的比例,并在每次接受修订前重新检查完整候选。
Best-of-N、束搜索和序列修订是一回事吗?
不是。Best-of-N 对完整候选评分选择,束搜索反复扩展并裁剪局部候选,序列修订则根据反馈修改已有候选。它们可以组合,但成本和失败模式不同。
纠错次数用完后应该返回什么?
应返回明确的未解决状态,以及保留的最佳已评估候选和真实检查结果。不能把上一个版本的验证结果附在新版本上,也不能把未通过检查的候选悄悄标成已接受。
来源与延伸阅读
- Self-Refine:迭代生成、自我反馈与修订。
- Reflexion:语言反馈与情景记忆。
- CRITIC:通过工具交互获得自纠错反馈。
- Large Language Models Cannot Self-Correct Reasoning Yet:受测条件下内在纠错的局限。
- DeepSeek-R1 报告:R1-Zero 与 R1 的训练区别。
- OpenAI 推理指南:API 参数与用量核算边界。