资讯动态

循环工程(Loop Engineering):大模型生成重复的检测与打破实践

发布时间:2026/10/21 8:08:22 来源:尧图企业网站定制
项目做到一半模型开始原地复读。我当时在生成一篇长文先是同一个词在连续三句话里反复出现接着整段复读最后连着章节标题都开始自我循环。那是我第一次认真面对一个事实在语言模型应用里最隐蔽的故障不是“答错”而是“转圈”。后来我花了几周时间专门研究这类问题发现可以把它系统化为一套方法论。这套方法论在社区里慢慢有了个名字叫 Loop Engineering直译过来就是循环工程。Loop Engineering 不是某个官方标准也不是某个框架自带的模块它是对“语言模型生成过程中循环现象的识别、检测、打破与设计”的一整套工程实践。它解决的问题很具体模型为什么复读如何快速定位是参数问题还是提示词问题怎么自动检测一段输出是不是陷入循环在 Agent 任务里怎么防止模型反复调用同一个工具反过来迭代自省、多轮修正这类良性循环又该怎么设计这篇教程就是把我自己的完整经验拆开从症状分类、根因分析、检测代码、打破手段到实战项目一步一步讲清楚。适合谁看如果你在用语言模型写长文本、做 Agent、搭内容生成服务并且被“输出重复”“跑偏”“原地打转”折磨过这篇文章可以省下你大量无头绪调参的时间。如果你只是好奇大模型为什么会抽风式复读前面的章节也能帮你理解背后的概率机制。1. 先给“循环”建档四种典型循环症状在动手修之前先把循环分清楚。不同症状看起来都像“重复”但根因和处理方式完全不一样。我把我实际遇到的案例都归了类识别方法都写在后面。1.1 词句层面的复读机循环这是最外层、最容易被肉眼发现的循环。典型表现是模型在某个时刻突然开始高频重复一个词、一个短语或一整句话。我遇到过一个很典型的例子让它写产品介绍写到第二段快结束时连续出现四次“需要注意的是”“需要注意的细节包括”“还要注意的是”。单独拎出来看每个句子都通顺但排在一起就像复读机。更严重的情况是同一句话原封不动重复好几遍中间不带任何变化。这种循环的识别特征很简单重复单位短通常是 3 到 10 个 token重复节奏密集基本出现在同段或相邻段内容没有信息增量和层次推进。1.2 语义层面的原地打转循环比字面重复更隐蔽。字面上句子不重复看起来也不像复读机但内容一直在同一个论点上绕圈信息量不增加。举个实际例子。我要求模型用五个角度论证某个技术方案可行结果第三个角度之后就开始反复强调“该方案具有显著优势”“性能表现优异”“能够有效解决问题”措辞一直在换但论据体系从头到尾只有那两三条。这就是语义层的循环。人类读者会感觉“这段好像讲过了”“怎么又回来了”。识别特征是用信息密度衡量时每隔固定长度就会出现与之前某个语义单元高度相似的内容句子的表层变化掩盖不了论点集合的收敛。1.3 链路层面的 Agent 死循环这个常见于带工具调用的 Agent 场景。模型拿到一个任务后会进入“思考—行动—观察”的循环正常情况下每轮行动应该推进任务但有时候模型会反复调用同一个工具、用同一个查询词乃至不断生成内容相同或相近的中间思考。我做某个检索类 Agent 时遇到过一次非常无语的情况模型反复搜索同一个关键词连续调了七八次工具观察结果一样思考结论也一样就是没有下一步动作。从服务端日志看Token 消耗在飙升但任务的进度条纹丝不动。识别特征是工具调用序列出现高重复模式同一个 tool 输入参数高度相似运行轮数超过预期却拿不到新信息。这类循环的代价是真实的计费数字不解决会直接烧掉预算。1.4 迭代层面的自我确认循环这个更深一些。发生在模型需要做批判性分析或反复修订任务的场景里。模型先给出了一个结论然后后面的所有步骤都在找证据强化这个结论而不是重新审视它。我让模型扮演项目复盘角色要求它先找到问题根因再给出改进方案。结果它从第一步开始就认定“流程不规范”是唯一原因后面每一步都在重复“流程不规范导致进度失控”“进度失控源于流程不规范”推理没有形成闭环只有自我确认的回路。识别特征是初始结论和最终结论完全一致中间没有任何“可能错了”“需要验证”的转折所有中间步骤都指向同一个方向推理链本质上是个环而不是一条线。这四种症状在实践中常常叠加出现语义循环的末端往往伴随字面重复Agent 死循环里也经常夹杂着语义打转。先建档的意义在于它能决定你之后往哪个方向排查。2. 循环是怎么被“喂”出来的生成机制、采样参数与提示词结构知道症状之后重点来了这些循环到底从哪来。我把根因分成三层可能和很多人的直觉不太一样。2.1 概率分布的自我强化重复内容为什么会“越陷越深”语言模型生成文本本质上是在每一步从全词表里按概率分布采样一个 token。问题在于这个概率分布会受到上下文里已有内容的影响。当上下文中出现过一个词这个词在后续位置的采样概率往往会变得更高。这就是循环的物理基础一旦模型在某个时刻输出了一个小范围内的重复模式这个模式就会作为上下文推高下一次继续输出相似内容的概率。重复的内容越多上下文中的重复信号越强模型就越容易继续复制自己。它是一个正反馈过程越陷越深。我常用一个类比来解释模型走出了一条浅沟然后它每一步都更倾向于顺着旧脚印走沟就越踩越深最后完全爬不出去。2.2 采样参数是循环的“旋钮”不是开关很多人一遇到重复就调 temperature但 temperature 本身是个双刃剑。调低了输出变得更加保守更容易走概率最高的那条路径反而可能强化重复模式调高了输出变得混乱短期看像是“跳出来了”但长期看可能会陷入另一种随机游走式的循环。真正直接对抗重复的是频次惩罚类参数它在计算概率时对已经出现过的 token 打一个折扣。还有一个配套的存在惩罚参数只要 token 出现过就惩罚不管出现几次。前者压制高频复读后者压制“出现过还得用”的倾向。我实测的经验是处理复读机循环优先动惩罚参数而不是 temperaturetemperature 只用于整体风格调节把它当成循环开关是常见误区。2.3 提示词没有“出口”开放式任务的结构缺陷很多循环不是生成层面的毛病是任务本身没给模型一个“什么时候该停下来”的信号。假设你直接让模型“写一篇关于数据可视化的文章”没有章节数量、没有字数上限、没有结束信号模型就会一直写下去。写到一定程度新内容枯竭但输出现象不能停于是就开始用已有内容打转把前面说过的换着法子再说一遍。这就像让一个人在没有出口的迷宫里一直走他最后一定会绕圈。提示词如果只有起点没有终点模型就只能在语义空间里画环。2.4 为什么循环总在长文本尾部爆发如果你观察过足够多的失败案例会发现循环的高发区在两处长文本的尾部以及上下文接近窗口上限的位置。这里面有个实际原因窗口越长前面内容对当前位置的“注意力”就被稀释得越厉害。模型在长距离依赖丧失后会倾向于关注最近的内容而最近的内容往往就是它自己刚刚生成的于是“看自己—复制自己—再看自己”的环就形成了。另一个长文特有现象是模型在任务后期已经没有“新计划”可用但它仍然处于生成模式。尾部爆发循环本质上是“任务目标已完成但生成信号未终止”的综合表现。所以你会发现纯粹的“调参解决循环”在短文本里好使在长文本里效果衰减很快。原因就是长文本循环有结构因素不是单纯的采样问题。3. 循环检测从人工肉眼到自动化指标修复循环的前提是能稳定地识别循环。人工肉眼看几千字找重复点既累又漏。我在实战里把循环检测做成了自动化模块核心就三招观察基线、自相似度、熵值监控。3.1 观察基线先回答四个问题动手写检测代码之前先人工建立一条观察基线。别嫌麻烦基线能帮你确定检测参数避免后面疯狂误报。我每次拿到复现样本会先回答四个问题循环单位是什么是词、句子还是语义块单位不同滑动窗口的 size 就不同。循环周期多长每隔几句出现一次相似内容周期决定窗口间隔和比较步长。循环发生在全文哪个位置开头还是尾部位置不同检测启动时机就不同。循环的相似度有多高是完全雷同还是“神似而形不似”这决定相似度阈值选多少。这四个问题回答完检测参数的初值基本就定了。3.2 自相似度检测滑动窗口与向量化字面重复容易检测难的是语义循环。我的做法是对文本做滑窗切分把每个句子或每个固定长度的段落转成向量然后计算相邻窗口内的平均相似度。相似度连续超过阈值就判定进入循环区间。向量化不一定要用重型模型。实测下来用字符级别的 n-gram 特征做 TF-IDF 向量化对于循环检测已经够用成本低跑得快。语义循环有一个特点即使措辞变了字符级的共现模式往往还是会留下痕迹n-gram 能捕捉到大部分情况。3.3 熵值监控生成质量塌陷的数值征兆第二个信号是熵值。我把生成过程中的 token 对数概率收集出来逐段计算平均熵。正常情况下生成内容的熵维持在一定区间一旦进入循环模型的概率分布开始高度集中熵值会明显下降。这个指标的好处是它不依赖语义判断纯粹看概率分布的形状。适合在服务端流式输出的过程中实时监控一旦发现连续多个片段的熵低于正常基线立刻触发预警。熵值监控没法单独作为判据因为它无法区分“模型在很确定地输出内容”和“模型陷入循环”需要和自相似度检测配合使用。自相似度负责抓文本结构特征熵值负责抓概率分布特征两个信号同时触发才算确认进入循环。3.4 一个可直接复用的循环检测器我把这些手段收敛成一个完整的检测类核心逻辑是“滑窗自相似度 文本熵计算”的二元判断。你可以直接拿去改。import math import numpy as np from typing import List, Tuple from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity class LoopDetector: def __init__( self, window_size: int 5, similarity_threshold: float 0.86, entropy_drop_ratio: float 0.65, min_text_len: int 200, ): self.window_size window_size self.similarity_threshold similarity_threshold self.entropy_drop_ratio entropy_drop_ratio self.min_text_len min_text_len self._vectorizer TfidfVectorizer( analyzerchar_wb, ngram_range(2, 3), ) def _split_sentences(self, text: str) - List[str]: parts [s.strip() for s in text.replace(\n, 。).split(。) if s.strip()] return parts def _window_similarity(self, sentences: List[str]) - float: if len(sentences) self.window_size * 2: return 0.0 vectors self._vectorizer.fit_transform(sentences) max_avg_sim 0.0 for start in range(0, len(sentences) - self.window_size, self.window_size): window vectors[start:start self.window_size] sims [] for i in range(len(window)): for j in range(i 1, len(window)): sims.append(cosine_similarity(window[i], window[j])[0][0]) avg_sim float(np.mean(sims)) if sims else 0.0 max_avg_sim max(max_avg_sim, avg_sim) return max_avg_sim def _entropy(self, tokens: List[float]) - float: if not tokens: return 0.0 probs np.clip(tokens, 1e-12, 1.0) probs probs / probs.sum() return float(-(probs * np.log(probs)).sum()) def is_looping(self, text: str, token_logprobs: List[float] | None None) - Tuple[bool, dict]: if len(text) self.min_text_len: return False, {reason: text_too_short} sentences self._split_sentences(text) sim_score self._window_similarity(sentences) entropy_score None entropy_flag False if token_logprobs: # 把一段文字的logprob序列等分成前中后三段比较稳定段与当前段的熵 third max(1, len(token_logprobs) // 3) stable_part token_logprobs[:third] current_part token_logprobs[-third:] if len(stable_part) 2 and len(current_part) 2: base_entropy self._entropy(stable_part) current_entropy self._entropy(current_part) entropy_score current_entropy if base_entropy 0 and current_entropy base_entropy * self.entropy_drop_ratio: entropy_flag True sim_flag sim_score self.similarity_threshold # 字面与语义循环以相似度为主熵值作为辅助强信号 is_loop sim_flag or (entropy_flag and sim_score 0.75) return is_loop, { similarity: round(sim_score, 4), entropy: round(entropy_score, 4) if entropy_score else None, sim_flag: sim_flag, entropy_flag: entropy_flag, }这个类每次接收一段完整文本和可选的 token 对数概率序列。第一次接入时建议把相似度阈值从 0.80 开始往上调到 0.90用你自己的数据校准一遍避免误报。4. 打破循环的三套打法参数侧、提示词侧、控制侧检测只是第一步。真正要解决的是“怎么能让模型不再转圈”。我的经验是分三路并行先动参数快速止血再改提示词解决结构问题最后加控制逻辑兜底。4.1 参数侧先把明显的复读机压下去面对复读机循环最直接的参数是频次惩罚系数。我通常从 0.3 开始调复读严重的拉到 0.6 到 1.0。效果非常明显但我有两条警告第一惩罚系数太高会让输出变得呆板。模型为了避免重复会强行换生僻词最后通畅度下降。不要在同一个任务上无脑拉满。第二存在惩罚系数对词句层面的复读帮助相对小但对“同一个概念反复出现”的语义打转有一定作用。它压制的是“出现即存在感”能让模型更快放弃旧论点。温度调节要用在正确方向上。复读机循环的处理顺序是先调频次惩罚再看是否需要降温度。如果模型在循环时还伴随着逻辑混乱可以尝试把温度从默认值调低 0.2 到 0.4让输出更收敛。反过来如果你在循环里看到的是反复走同一条高概率路径可以尝试临时加一点温度让它跳出概率陷阱但要配合惩罚参数防止跳进新的随机循环。4.2 提示词侧给生成任务设计“强制出口”参数只能止血提示词结构才决定循环会不会复发。我的核心心法只有一句话永远给模型一个明确的“出口信号”。什么叫出口信号就是提示词里明确写着什么时候该结束、以什么格式结束、内容边界在哪里、不许重复什么。我在写长文生成提示词时会明确要求模型按“大纲—分节—总结”的三段结构走并且强制每节只能写若干个要点前面章节用过的论点不允许再次出现。实际上这一步比调参更根本。我把提示词里的格式约束比作“逃逸通道”模型生成过程中的概率压力再大都会被这个“必须收尾”的信号及时拽回来。另一个有效技巧是“反驳清单”。在批判性任务里我要求模型“先列三条支持你结论的证据再列三条反对你结论的证据最后再重新给出结论”。这套做法能有效把自我确认循环打断因为“列出反对证据”这一步天然就把模型推下了原来的位置。4.3 控制侧解码约束与后处理兜底参数和提示词都做了仍然会有漏网之鱼。控制侧就是最后一道防线。我在服务里加了三道闸。第一道是 stop 参数在生成接口里指定一组终止序列一旦模型开始输出某些固定的“车轱辘话”直接截断。第二道是 max_tokens 约束给每次生成设定合理的上限避免无界输出。第三道是生成后的循环检测把第 3 节的 LoopDetector 接在流式输出后面实时判断输出是否异常异常就触发重写或降级策略。重写策略我建议这样设计检测到循环后不要直接换参数重新生成全文。代价太大而且可能引入新的偏差。更稳的做法是定位到循环开始的段落只截取循环之前的内容作为上下文调整提示词里的补充约束从循环起点重新生成。实现思路是保存生成过程中的章节边界监测到循环后回滚到最后一个有效章节尾重写该章节。4.4 为什么“只能缓解不能根除”概率模型的本质做完了三套打法我还是要泼一盆冷水循环问题不存在一劳永逸的解法。因为语言模型本质上是一个概率模型。它的生成过程天然包含自回归的偏差只要上下文窗口有限只要 token 的采样概率受历史内容影响重复路径就永远存在。你不可能“修好”它只能“管好”它。参数是旋钮提示词是约束控制逻辑是护栏但护栏之内依然可能出现打转的个案。这点认知很重要。很多团队在循环问题上卡壳不是技术不到位而是目标定错了以为要让模型“永不重复”。实际上能做到的是“概率大幅下降 异常自动拦截”只要这两点到位用户侧基本感知不到这就已经合格了。5. 项目实战构建一个带循环守卫的多轮文章生成器讲完方法论进入实战。我这里分享一个我做过的模拟项目项目代号叫“带循环守卫的多轮文章生成器”核心目标是自动生成一篇 3000 字以上的多章节长文并且在生成过程中自动检测和修复循环症状。5.1 需求拆解与整体设计先看需求清单。输入是一个主题词和一个大纲列表。输出是包含四个以上章节的完整文章每个章节至少 600 字。要求是在无人看守的情况下,不出现复读机循环、语义打转和章节间重复。我把它拆成四个模块大纲规划模块根据主题生成各章节大纲并约定每章主题边界。章节生成模块基于大纲逐章生成内容每章有独立的出口信号。循环守卫模块用第 3 节的 LoopDetector 检测每章输出。修复调度模块检测到循环后调用重置策略重写对应章节。整体流程是大纲模块先生成章节列表然后进入逐章生成循环。每章生成完毕立即送入循环守卫守卫判定正常就把章节内容追加到上下文中继续下一章判定异常就回滚到本章开头用修复策略重试。重试超过两次仍失败就把当前章节拆成两个更小的子章节分别生成。5.2 大纲规划模块先定边界再写正文大纲模块是整个项目的隐性核心。我观察到循环问题最严重的场景是模型“不知道本章到底该写什么”。大纲规划模块的作用就是把“模糊的写作主题”转换成“带边界的章节描述”。我给这个模块设计了一个输出格式每章包含章节标题、核心论点列表、本章禁止出现的论点引用列表。禁止列表是从前面所有章节的论点里抽取出来的模型在写当前章节时明确知道自己不该碰哪些内容。这个设计的直接效果就是从根本上减少了语义层循环的可能。因为循环的本质是“无话可写但必须写”而禁止列表给了模型一个“不写什么”的明确指引比“写点什么”更容易约束生成路径。5.3 章节生成模块与循环守卫的联动生成模块本身不复杂就是按大纲提示词逐章调用模型接口。关键在于它和循环守卫的联动方式。我实现了一个主循环伪代码如下def generate_article_resilient(topic: str, outline: dict, detector: LoopDetector, max_retries: int 2) - str: context_parts [] for chapter in outline[chapters]: chapter_text None retry_count 0 while retry_count max_retries: prompt build_chapter_prompt( topictopic, chapterchapter, historycontext_parts[-3:], # 只带前几章防止窗口过长 forbidden_pointschapter[forbidden_points], ) result call_model_api(prompt) if result.skipped: # 模型判断该章无需展开或内容重复 chapter_text None break ok, meta detector.is_looping( result.text, token_logprobsresult.logprobs, ) if not ok: retry_count 1 # 修复策略临时提高惩罚系数并新增一条“不要重复以上论点”的提示 result retry_with_higher_penalty( promptprompt, penalty_increase0.2 * retry_count, extra_instruction用全新的例子支撑论点禁止重复你刚才写过的句子, ) ok, meta detector.is_looping( result.text, token_logprobsresult.logprobs, ) if ok: break chapter_text result.text if chapter_text is None: continue # 跳过该章或降级为纯大纲 context_parts.append(chapter_text) return \n\n.join(context_parts)这个代码的关键点有三个第一上下文只带前几章不能全量累积。长上下文本身就是尾部循环的诱因我在实战里会限制 history 不超过三章再早的内容用大纲模块抽出的论点摘要代表。第二重试不是无脑重生成而是“提高惩罚系数 追加约束指令”的双重干预。把重试当成一次有导向的微调成功率比每次都原样重来高很多。第三模型如果返回“本章内容已被前面覆盖”我直接接受空章节并跳过。这个设计刚开始我觉得会损失内容实际测试后发现它反而是防止章节间语义循环的有效工具。5.4 实测效果对比我跑了一组对比实验。用同一个主题“远程办公工具的发展趋势”两组配置分别是普通长文生成以及带循环守卫的生成。普通配置在生成第三章节时开始重复“远程办公提高了灵活性”到第五章节出现了连续三段的字面重复全文最终停在 5000 字左右实际有效信息大约只有 3000 字。带守卫的配置第二章节检测到一次语义循环触发了重试重试后成功绕开第四章节再次检测到相似度 0.91 的语义循环触发二次重试并且采用了“拆章节”降级策略把原章节拆成两个小章节最终顺利完成。全文五个章节整篇没有出现连续重复段落有效信息密度大幅提升。值得一提的是守卫启动的两次修复单次耗时只增加了 15% 到 25%在可接受范围内。如果完全不做检测表面上耗时低但生成结束后还得人工返工总成本其实高得多。6. 从“打破循环”到“设计循环”Loop Engineering 的另一半如果只把 Loop Engineering 理解成“除虫”那就浪费了它另一半的价值。循环不全是坏事很多有用的能力恰恰要依赖“让模型自己转圈”。6.1 良性循环迭代自省与多轮修正语言模型单次输出往往有短板但只要让它“自己检查自己”结果会有明显改善。我把这个过程看作一个良性循环生成初稿对自己打分找出薄弱点重新生成再打分直到分数达标。关键是要给这个循环一个明确的“进化方向”。我用的方案是让模型输出一组结构化自省意见逻辑问题、事实风险、冗余内容、表达不足。再把这份意见作为下一轮生成的补充指令。这样每一轮都会真的变好一点点而不是换个姿势重复。不过要提醒自省循环有效的前提是初始任务本身有足够的信息可挖掘。如果任务本身信息不足那自省循环会退化成自我确认循环越修越偏。解决办法是在循环里加入外部检查点比如让另一个模型做交叉验证。6.2 带终止条件的循环哨兵 token 与预算约束设计良性循环时最容易犯的错是忘记设终止条件。循环没有出口就是无底洞。我在所有迭代型任务里都会强制加“预算”和“哨兵信号”两道约束。预算很好理解最大轮数、最大 token 消耗、最大耗时任何一个超限就强制终止返回当前最优版本。哨兵信号是指在提示词里写清楚“当且仅当你认为文本满足以下三个标准时输出 END”。模型一旦输出 END整个循环就终止。这两道约束配合使用等于给循环装了一个自动刹车。我从实践中得到的体会是良性循环设计里“如何退出”比“如何进入”更重要。6.3 一个可复用的生成器验收清单最后分享一个我自己整理的多轮生成任务验收清单拿来即用[ ] 每轮生成是否都有明确的上下文边界还是无限累积[ ] 每轮是否给模型提供了上一轮的客观反馈信号[ ] 循环内是否有最大轮数、最大 token 消耗的硬性上限[ ] 是否设置了可识别的终止输出信号哨兵 token[ ] 是否对最终输出做了循环症状检测而不是只看接口是否成功[ ] 检测到循环时是否有预设的修复策略而不是人工介入这套清单在新项目上线前过一遍能拦下大部分隐性循环问题。我做这个项目最大的体会是和模型打交道不能幻想它像确定性的代码一样稳定。它更像一个情绪化的写手写顺了能超常发挥写僵了会原地转圈。Loop Engineering 的本质就是一套管理“转圈”的机制承认转圈不可避免但让转圈变得可见、可控、可终止。把这些机制做扎实之后模型应用的可信度会有一个质的提升这也是我在后续所有项目里一直保留循环守卫模块的原因。

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

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

免费获取报价 →
↑