先聊个我最近经常遇到的局面。我在做基于大语言模型的自动化任务时,最烦的不是模型不懂规则,而是同一个问题让它重新生成一遍,十有八九还是犯一模一样的错。你给它加提示词、换示例、调温度,折腾半天,结果它照样忽略某个边界条件,照样生成乱七八糟的字段。后来我换了个思路:不再把生成当"单次调用",而是把模型每一次的输出连同反馈一起塞回上下文,让它看到自己的行为引发的后果,然后在这个反馈基础上重新生成。这个思路被圈里人叫做 in-context feedback learning —— 直接说就是"把反馈信号也作为上下文的一部分,让模型在推理阶段自我修正"。这篇文章我打算把这个方法掰开揉碎,从原理到落地,附上我实际跑过的案例和踩过的坑,适合正在做 LLM 应用开发、Prompt 工程,或者被模型"屡教不改"折磨的读者。你不用上梯度、不用微调,就能让模型在一轮轮对话里越改越准。
1. 一次生成定生死:单轮输出才是当前应用最大的隐性瓶颈
很多团队用 LLM 的方式,本质上还是"投喂需求,等待答案"。需求描述得不够细,就继续往 prompt 里堆描述;效果不对,就换一个模型、调一调 temperature。这种方式看似灵活,实际上是把大模型当作一次性的"答案生成器",用完就扔。可模型的输出本质上是一个概率采样过程,你那句"请仔细一点"根本改变不了它在隐空间里的出发位置。真正能修正行为的,不是抽象的"仔细",而是某个具体的、由它的输出引发的外部信号。
1.1 三个日常场景里的"回环缺失"
我先把最常见的三个场景摆出来。第一个是代码生成。让模型写一个处理 JSON 的函数,它生成了,丢到 Python 环境里一跑,报错说某个字段可能不存在。你重新让它写,它大概率还是无视这个分支,因为 prompt 里没有告诉它"上一个版本是因为什么报错的"。第二个是文案生成。你让模型给某个产品写介绍,它写出一版,你说"不够有感染力",再来一遍,它可能只是把形容词换得更华丽,但结构问题、信息顺序问题原封不动。第三个是数据分析。模型给了一个结论,你其实知道它用了错误的数据口径,但你没有把"口径错误"作为反馈喂回上下文,于是它下一轮依然沿用错误的假设,产生连锁跑偏。
这三个场景的共同点是什么?它们的成败都取决于"第一轮之后有没有反馈通道"。现实任务几乎不存在"一次生成直接完美"的,总是要经过检查、修改、再验证。但当前的调用方式把"生成"和"修正"割裂开了,模型在两轮之间没有记忆,也就没有学习。
1.2 从 In-context Learning 到 In-context Feedback Learning
熟悉大模型的朋友都知道 In-context Learning(ICL),就是在 prompt 里放几个示例,让模型模仿示例的模式来执行新任务。它的机制,本质上是通过上下文文本改变模型内部生成序列时的条件分布。你给两个"输入-输出"示例,再给一个新输入,模型会顺着示例的格式、推理路径"续写"下去。
但 ICL 有一个天生的盲区:示例描述的是"别人怎么做是对的",模型并不清楚"我自己刚才做错了什么"。In-context feedback learning 和 ICL 的区别就在这里——feedback learning 提供的不是静态示例,而是针对模型自身输出的反馈信号。这个反馈可以是编译器报错、测试结果、用户的修改痕迹,甚至是另一个模型的评判。它们不改变模型权重,只改变生成时的条件上下文,却能对后续输出产生方向性影响。你可以把它理解为"考试时老师用红笔标注出你的错误步骤,然后让你在同一张答卷上重做"。这比单纯给一份标准答案更能帮学生定位问题,因为标注是针对个体错误路径的。
2. 反馈进上下文的机制:模型为什么会越改越好
说清楚了概念,接下来得回答一个关键问题:反馈为什么对 Transformer 架构有效?我的理解是,语言模型生成后面的 token 时,会对前面出现的所有 token 做注意力加权。当反馈信息,尤其是包含"错误原因 + 正确动作"这类强因果提示的文本出现在上下文里,它就像一个高优先级的路标,会在后续解码时持续施加影响。反馈不直接修改参数,但它改变了模型"接下来最可能生成什么"的条件。
2.1 反馈进入 Prompt 的本质:给模型一个"行为校正信号"
我用一个特别生活化的类比来解释。想象你在学做菜,第一次炒出来的菜太咸。有两位师傅指导你。第一位师傅说:"再试一次,努力做得更好一点。"你会很茫然,不知道该从哪改。第二位师傅说:"你刚才放了 20 毫升酱油,这道菜只需要 10 毫升;另外你是在快出锅前才加的盐,应该在腌制时放。"你照着这个反馈重做,大概率立刻就能改善。
大模型也是一样的。如果你只说"请重新生成一个更好的版本",模型根本没有足够的信息判断"好"与"不好"的边界在哪里。可一旦你将具体的失败模式、报错信息、错误步骤编号放入上下文,模型就能以这些信息为锚点,主动绕开原来的错误路径。这背后其实是语言模型对文本内"因果链条"的强建模能力:它擅长根据上一段分析推导出下一段行动。你给它一个"诊断书",它自然能写出"治疗方案"。
2.2 三种我在生产中验证过的反馈形态
反馈不是只能来自人,不同任务的反馈源差别很大。我根据实践把它们分成三类,都是可以放进上下文的信号。
第一类是真值反馈。我把模型输出和标准答案做一个 diff,或者把输出喂给校验函数,然后把差异部分直接贴在 prompt 里。典型例子是结构化抽取任务:模型把一个日期字段抽错了,我把标准格式和模型输出放在一起,让它自己看差别。这类反馈最硬核,效果也最可预期。
第二类是执行反馈。这种在代码和 Agent 场景下尤其常见。模型写的代码拿去运行,解释器报错;模型调的 API 返回了异常状态码;模型生成的 SQL 执行结果不符合预期。把这些执行结果原样塞进上下文,模型往往能根据报错栈反推自己的逻辑错误。这类反馈的优点是客观、具体,缺点是模型容易被一个表面报错带偏,需要在反馈里补充"期望行为"以防止它去修一些无关的东西。
第三类是人类偏好反馈。用户对生成结果的划线编辑、点赞、批注,都被我结构化为简短的文本信号。比如"第三段论证偏离主题,请回到'成本优先'框架"。这类反馈最软,但最高频。它的关键在于把用户的情绪化表达翻译成模型能执行的动作指令。
2.3 反馈要"明确且可操作",而不是单纯说"你错了"
这里我得强调:不是所有反馈都能提升模型表现。我见过最没用的反馈就是"输出质量低,请重新生成"。模型收到这种反馈后,只会把文本整体重写一遍,甚至把本来对的部分也改错。我总结过一个反馈质量的分级,从低到高大概是这样:
| 反馈等级 | 示例 | 对模型行为的引导力 |
|---|---|---|
| 无反馈 | 无 | 模型凭概率重新采样,随机性大 |
| 结果型反馈 | "你错了" | 模型知道要改,但不知从何改起 |
| 行为型反馈 | "你忽略了空值分支" | 模型能在特定维度上主动规避 |
| 归因型反馈 | "因为第 2 步没有判空,导致后续字段全部 KeyError" | 模型能重建因果链,一次性修正所有连带错误 |
我几乎总是要求反馈里至少包含两部分信息:一是可观察到的"错误现象",二是能指引方向的"错误原因或修正策略"。只给现象,模型可能会头痛医头;只给原因,模型可能不知道现象有多严重。两者都齐全,才能在解码时形成足够的约束力。
3. 五步闭环实操:把反馈学习流程落到具体项目里
原理讲完总要落到工程实现。我目前实践下来,一个完整的 in-context feedback learning 闭环分五步:收集反馈、结构化整理、重组上下文、再生成、校验结果。这几个步骤看似简单,但每一步都有不少细节,细节决定了最终效果能好到什么程度。
3.1 闭环流程总览:从"生成"到"修正"的循环迭代
一个典型流程长这样:我先用一个基础 prompt 让模型生成初步结果。这个结果会被送到外部的检查器或人工手里,产生反馈信息。接着我把原始 prompt、模型上一次的输出、以及新产生的反馈拼接成一个新的 prompt,再让模型生成修订版。修订版再次送检,如果还有问题就继续迭代。
这个流程在概念上非常接近控制论里的闭环控制,输出被测量后馈送到输入端,形成修正。和那些"调 prompt 调半天"的静态做法相比,feedback learning 的核心优势在于:每一次迭代都是针对当前模型的实际行为反馈,而不是凭空猜模型哪里会出错。
我建议一开始不要把迭代轮数设成固定的"3 次"或"5 次",而是设一个"校验是否通过"的停止条件。比如代码任务,以编译通过和测试全绿作为停止条件;抽取任务,用字段完整性校验作为停止条件。只有校验通过,才结束循环。如果不通过,就把新的失败信息继续加入上下文。
3.2 反馈缓冲区的设计:保存哪些内容、丢哪些内容
既然要把反馈放进上下文,就绕不开 token 窗口限制。我专门维护了一个"反馈缓冲区",这个缓冲区其实是一个结构化的字符串,每次迭代结束时更新。
缓冲区里我固定保留三类信息:任务原始要求、当前模型输出摘要、按时间顺序排列的历史反馈列表。任务信息无论如何都在,防止模型在迭代中忘了初心;模型输出摘要控制在 100 token 以内,只记录输出形态;历史反馈列表是重点,我每次给每一条反馈编号,并注明它属于哪一轮。例如:
第 1 轮反馈:输出缺少空值判断,导致 KeyError; 第 2 轮反馈:空值已处理,但仍未覆盖字符串类型输入,断言失败; 第 3 轮反馈:字符串类型已覆盖,但返回值格式与约定不一致。
注意我给每条反馈都标明了轮次和当前状态。这看起来麻烦,但非常有价值,因为模型能据此推断"哪些问题已解决、哪些还在局限"。
3.3 重组成可复用的修正 Prompt 模板
缓冲区设计好之后,重组成 prompt 就有固定套路了。我最终使用的模板长成下面这样,你可以直接抄去用,语言模型是 text 场景:
【任务原始要求】 {base_instruction} 【模型上一轮输出】 {previous_output} 【历史反馈记录】 {feedback_buffer} 【本轮修正目标】 请根据以上反馈,分析失败原因,输出修正后的完整答案。 要求: 1. 不得重复历史反馈中已指出的错误; 2. 保留上一轮输出中已正确的部分; 3. 对修正逻辑给出 2 行以内的简要说明。这个模板的关键在于"保留上一轮正确部分"这一条。很多失败的迭代就是因为模型重写得太彻底,把本来对的代码逻辑推倒重来,引入新 bug。加上这条要求之后,模型会倾向于做"外科手术式修改",而不是整体推翻。
3.4 防止模型"改过头":回归检查与正确片段锁死
有了模板,最容易踩的坑就是改过头。我打个比方,模型原本的代码有 4 个函数,其中 3 个是对的,只有 1 个有问题。你把反馈给它,它重写时可能把 4 个函数全改了,而且改完 2 个变成错的。解决办法除了在模板里写明"保留正确部分",我还会额外做一层人工保险:显式地把"已知正确的内容片段"放进一个不可修改区,并在 prompt 中声明。
比如生成代码时,我可以在 prompt 里写:"以下片段已验证正确,必须原样保留:{correct_fragment}"。生成文档时,则可以把用户认可的开头和结尾锁死。这个思路实际上是把"回归测试"的思想搬到了 prompt 工程里,防止模型在修正一个 bug 时顺手破坏其他功能。
4. 三条实战案例全记录:修代码、改文案、纠推理
讲了这么多,不如直接看三条我在真实任务里跑过的案例。每一条我都记录了输入、反馈、输出变化和最终效果,尽量还原当时迭代的过程。你对照自己的项目,应该能找到相似的影子。
4.1 代码修复:让模型拿着编译报错去改,而不是赌运气
前阵子我让模型写一段 Python 脚本,从嵌套 JSON 中提取指定字段。第一版代码长这样:
def extract_field(data, path): keys = path.split('.') for key in keys: data = data[key] return data这个函数看起来很直白,但一跑就崩:如果中间某个 key 不存在,直接 KeyError。我把这个输出连同报错信息:
KeyError: 'address'一起放进上下文,并附上反馈:"函数未处理中间键缺失的情况,当 path 指向的嵌套层级中任何一级不存在时,应返回 None 而不是抛异常。"模型第二轮给出的代码是:
def extract_field(data, path): keys = path.split('.') for key in keys: if not isinstance(data, dict) or key not in data: return None data = data[key] return data这个版本已经非常接近完美防御了。你看,反馈中我不仅给了报错,还给了期望行为——"应返回 None"。这就避免模型去改一些无关紧要的变量命名。整轮迭代只花了一次额外调用。
4.2 文档与文案打磨:把用户的"划线编辑"变成上下文中的数值化信号
代码案例相对容易,因为编译器给了精确的错误位置。文案类任务更麻烦,反馈往往是定性的。我做产品说明文案时,第一次输出被老板划了三处:产品核心卖点埋得太深、技术参数堆砌太密、缺少使用场景联想。我把这三条点评结构化,还加了一句:"当前文案共 420 字,目标控制在 350 字以内。"
模型第二轮输出时,明显把卖点提到了第一段,技术参数合并成一行,在中间补了一个"办公场景"。我没有输出前那么直奔主题,但这次已经能让老板靠它做基础沟通了。这个案例给的经验是:文案反馈要做到"可操作",得把主观描述翻译成具体的结构动作,比如"第 2 段移到第 1 段之后""删除第 4 段的技术参数表""新增一个具体用户场景"。模型对"位置变化""增删模块"这类指令的执行准确度,远高于对"更有感染力"的理解能力。
4.3 数学推理题纠错:反馈需要引导到"过程"而不是"答案"
最后是推理类任务。我拿一道鸡兔同笼变体题测试,模型第一轮直接把步骤跳了,答案也给错。如果我只给反馈"答案是错的,请重算",模型有可能换一种算法,但还是错。所以我的反馈写得更细:
"第一步设未知数正确,但第二步你的方程左边少了脚数系数。请补全所有步骤的推导,再给出答案。"
模型第二轮会把方程展开、合并同类项、代入验证都写出来,最后答案自然就对了。推理任务和代码任务本质类似,模型犯错点往往在某个步骤的"跳变"上。反馈应该明确指出跳变的步骤,并强制模型补全过程。这比单纯告诉它"用另一种方法"有效得多,因为模型并不缺方法,它缺的是对自己的步骤做结构化检查。
5. 和相邻技术方案的边界:总有人问这和微调、RAG、Self-Refine 有什么区别
在我分享这个方法之后,收到最多的评论是:"这不就是微调吗?""这不是 Self-Refine 吗?""和 RAG 有什么区别?" 我必须认真对待这个问题,因为方案边界搞清楚,你才能选对工具。它们确实都在做"给模型更多信息"这件事,但信息进入系统的位置和方式完全不同。
5.1 先划清界限:这不是微调,也不是 RLHF
最根本的差异是:微调会更新模型权重,而 in-context feedback learning 完全在推理阶段进行,模型参数动都不动。权重更新意味着模型永久改变了行为基线,适合那种高频、稳定、可重复的错误场景,比如一个客服系统要把公司内部专有名词统一翻译。但微调的代价是训练数据准备、GPU 资源、模型版本管理,一套下来没有两周也得一周。
反馈学习则更像是"测试时修正"。它的成本就是多几次 prompt 调用,几分钟内能看到效果。我实际用下来,对处理频率低、每次错误模式都不一样的任务,反馈学习的性价比碾压微调。比如你要给几十份不同格式的合同做抽取,A 公司的错误模式和 B 公司的错误模式完全不同,你不可能为每一家微调一个模型,但你可以给每一家都套上同样的反馈循环。
| 对比项 | 微调 | In-context Feedback Learning |
|---|---|---|
| 参数更新 | 是 | 否 |
| 部署成本 | 高,需要模型版本环境 | 低,纯 Prompt 级改动 |
| 错误模式变化快时 | 需要反复训练,慢 | 直接改 prompt 里的反馈即可 |
| 适用场景 | 高频、稳定、通用性强的模式 | 低频、个体差异大、快速迭代场景 |
5.2 与 Self-Refine 的微妙关系:外部反馈比自我批判更可靠
Self-Refine 的思路是让模型自己评价自己的输出,生成反馈,再修订。它和 feedback learning 很像,区别在于 feedback 的来源。Self-Refine 的反馈来自模型自身,本质上依赖模型已有的知识去发现问题,但模型往往"不知道自己不知道",尤其是遇到领域性很强的错误时,它连问题在哪都识别不出来。
In-context feedback learning 并不排斥自我批判,但它强调外部信号的重要性。编译器报错、测试断言、用户否定、另一个模型的判断,这些都是模型自身视角之外的信号,可靠性更高。我的建议是两者组合使用:先用外部检查器收集硬错误,把硬错误喂回上下文,再让模型基于硬错误做一段自我总结。比如代码场景,我会在 prompt 里写:"根据上一轮报错,请先分析根因,再给出修订。" 这一步本质上是把模型当作自己的调试助手,但信息来源已不再是凭空脑补。
5.3 与 RAG 的关系:反馈是经验,RAG 是知识
很多人想到 RAG,第一个念头是"它也往 prompt 里塞东西"。但 RAG 塞的是检索来的外部知识,比如公司文档、产品手册、领域论文,解决的是"模型不知道这些信息"的问题。而反馈学习塞的是模型自己的行为后果,解决的是"模型知道了知识,但没按照知识正确执行"的问题。你可以边检索边反馈:RAG 负责让模型获得知识,feedback 负责让模型在使用知识的过程中不断被校正。两者是互补的,不冲突。举个例子,让模型写一个产品指标解释,RAG 先给它几条权威口径,反馈循环再根据它写出来的表述偏差进行第二轮修正,这样才能同时保证知识来源正确和表达准确。
总而言之,边界认识非常重要。微调、RAG、Self-Refine、feedback learning,解决的是不同层面的问题。feedback learning 的核心优势在于"低成本、快速、针对个体行为",它不需要训练资源,也不需要额外的知识库,只要你能把某种形式的反馈文本化,就能进入这个循环。
6. 落地时最常见的四个坑,以及我的应对策略
既然是真实验证过的方案,踩过的坑也得老实交代。以下四个问题是我在实际落地时反复遇到的,几乎每个接入 feedback learning 的项目都逃不掉,你可以提前做好心理建设和工程准备。
6.1 上下文越滚越胖:token 膨胀与成本失控
这是最直接的问题。每多一轮迭代,历史反馈 + 输出摘要就多几百 token,迭代到第 5 轮时,prompt 可能已经是原始长度的 3 倍。大模型的计费是按 token 来的,多轮迭代时间越长,成本越高,同时响应延迟也会变大,因为模型要处理一大段冗长的历史。
我的策略是给反馈缓冲区设一个硬上限,比如只保留最近 3 条反馈,更早的反馈压缩成一行摘要。另外,当模型连续两轮都在解决同一个问题时,我会认为它对这个反馈的理解已经接近稳定,没必要继续重复灌输,直接合并成一条终端反馈:"此前已指出 XX 问题,请确认本轮输出不再包含该类错误。" 这样既保留了上下文约束力,又控制了长度。
6.2 反馈顺序影响效果:模型只关心最近的指责
Transformer 的注意力天然有近因偏好,输入序列末尾的信息往往对后续生成影响更大。我测试时发现,如果你把新一轮反馈排在旧反馈后面,模型会"只盯着新反馈"修正,忽略了之前一直没解决的旧问题。比如第 3 轮它修好了空值问题,第 4 轮你又反馈了一个格式化问题,它可能顺手把空值处理又弄丢了。
我解决这个问题的办法是:把"已解决的问题"和"未解决问题"分两个区段写在 prompt 里。已解决区段用一句话概括"这些问题已修复,保持现状",未解决区段放在 prompt 更靠后的位置,用更强烈的指令语气。这相当于给模型一个记忆清单,让它知道哪些要保住、哪些要改动。
6.3 模型把反馈当作任务要求而非行为描述:反馈污染
这是个非常隐蔽的坑。当你把历史反馈一股脑塞进上下文后,模型可能会把反馈本身当成当前任务的一部分来执行,导致输出形式被反馈文本"污染"。比如我在文案任务中写"第二版应该增加使用场景",模型在第三版的开头直接说"根据您的反馈,我增加了使用场景",而不是直接给改写后的文案。显然,模型误以为"复述反馈"也是输出的一部分。
我处理反馈污染的办法是给输出施加更严格的格式约束,在 prompt 末尾明确声明:"输出只包含最终答案,禁止任何解释性标题、反馈回顾或对话开场白。" 这一行强指令能有效压制模型把反馈当成答案内容的倾向。
6.4 延迟不是免费的:需要做合理的工程取舍
每一次反馈重生成,都是一次完整的前向推理,这在大模型应用里意味着多几百毫秒甚至几秒的延迟。在后台跑离线批量任务时无所谓,但做在线服务时就必须考虑。我在一个实时问答服务里试过这种循环,用户等不及 4 轮迭代,于是我把迭代上线改成"最多 2 轮,且第一轮必须强校验,第二轮才开始反馈重生成"。
这样虽然牺牲了一部分极限效果,但延迟可接受。我的原则是:在线场景优先保证实时性,用更高质量的初始 prompt 降低需要反馈的概率;离线场景可以大胆用满 5 轮甚至更多,因为延迟无所谓,准确率优先。
我在实际项目中体会最深的一点是:in-context feedback learning 并不是某种花哨的算法,它更像一种工程习惯——每次生成之后都多问一句"结果回去之后,能不能给模型一个明确的、可执行的反馈"。当你开始把一个一个外部信号组织成上下文中的修正指令时,你其实是在把一次性的工具调用,变成一条有记忆、能迭代的行为链条。这个方法不一定适合所有任务,但我建议你先挑一个错误模式相对清晰、反馈容易结构化的场景试起,比如代码报错修复或字段抽取校验,跑通一次闭环,你就能感受到它和"反复重新生成"之间的差别。做 LLM 应用到最后,拼的往往不是谁调的模型大,而是谁手里的反馈回路更密、更清晰。