核心摘要

大模型自纠错的价值取决于反馈能否指出真实缺陷,以及后续检查能否确认修复。模型说“已经改好了”不能替代验收。工程上应分离候选生成与接受逻辑,保留已评估版本、拒绝回归,并同时统计修对了多少错误、改坏了多少正确答案,判断实际收益。

目录

自纠错能说明什么

自纠错是针对已有答案执行反馈与修订的过程,结果可能变好、不变,也可能变差。把流程命名为“纠错”,并不等于它已经成功。

在应用中,应把四件事分开:

  1. **发现错误:**证明某项要求没有满足。
  2. **定位问题:**指出相关字段、结论或行为。
  3. **提出修复:**生成针对问题的修改。
  4. **验收候选:**按完整契约重新检查修改后的结果。

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”
有依据的问答 缺乏支持的结论与相关原文 另一段流畅回答
任务规划 违反容量或权限的具体约束 更高的自报置信度

还应把硬约束与偏好分开。币种正确、目标已授权、必填字段存在,不能被更高的文风分数抵消。模型评审可以辅助判断可读性,但不能覆盖未通过的强制检查。

flowchart TD A["初始候选"] --> B["按固定契约评估"] B --> C{"全部必需检查通过"} C -->|是| D["接受当前这个候选"] C -->|否| E{"还有修订预算"} E -->|否| F["返回未解决及已评估候选"] E -->|是| G["根据失败证据修订"] G --> H["评估新候选"] H --> I{"有改善且无回归"} I -->|是| J["替换保留候选"] I -->|否| K["拒绝本次修订"] J --> C K --> E

反馈应绑定候选版本、任务版本、验证器版本和来源快照。任意一项变化后,沿用旧验证结果都可能造成误判。文本差异有助于检查改动,但变化量不是正确性指标:一个负号足以改坏答案,大幅重写也可能真正修复问题。

用 Go 实现带回归检查的修订循环

下面的 Go 1.23 示例为发票摘要实现保守的验收策略:保留最佳已评估候选,不允许丢失已有通过项,并且每个修订使用前都重新检查。修订函数返回预设候选,仅用于复现控制逻辑,不调用真实模型,也不测量模型效果。

币种、小计和税额来自可信输入;生成结果只包含币种和合计。金额使用整数最小货币单位,避免浮点舍入,并拒绝非法或会溢出的来源金额。

保存为 main.go 后执行 go run main.go。

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)
}

预期输出:

text
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 对完整候选评分选择,束搜索反复扩展并裁剪局部候选,序列修订则根据反馈修改已有候选。它们可以组合,但成本和失败模式不同。

纠错次数用完后应该返回什么?

应返回明确的未解决状态,以及保留的最佳已评估候选和真实检查结果。不能把上一个版本的验证结果附在新版本上,也不能把未通过检查的候选悄悄标成已接受。

来源与延伸阅读