你有没有遇到过这种情况:同一个问题来来回回追问模型好几遍,得到的答案几乎是一个模子刻出来的——语气相同、结构相同、连举例都雷同。这不是幻觉,而是默认解码策略的必然结果。今天想认真拆一下 Brainstorming 这类“发散式生成”的实现机制:模型怎么才能像人开脑暴会一样,用同一个输入种子生成一堆彼此不同、甚至方向迥异的想法。
这篇文章适合两类人。一类是做 AI 应用开发的工程师,想给自己的产品加一个“帮用户想点子、列方案、出创意”的功能;另一类是对大模型生成原理好奇、想真正理解“为什么调一调参数就能得到不同的结果”的读者。我会从解码机制层面把温度采样、核采样、重复惩罚各自做了什么讲透,再给出一套可以照着写的多路发散生成流程。读完你能回答三个问题:随机性到底从哪里来?多样性靠什么控制?如何把一批发散结果变成可用的收敛答案?
1. 让模型从“复读机”变成“头脑风暴会”:核心设计思路
1.1 默认解码为什么总偏向“安全答案”
要理解 Brainstorming 机制,先得搞清楚模型的默认行为为什么那么“保守”。大模型的文本生成是逐 token 进行的:每一步都会计算出一个概率分布,然后从中选取一个 token 作为下一个输出。最常见的默认解码方式是贪心解码,也就是每一步都挑概率最大的那个 token。问题在于,概率最大的路径往往是语料中出现频率最高的路径,而高频路径从“最安全”滑向“最无聊”是必然的。
我在实际测试里有个很深的体会:中文互联网语料里,“首先”“其次”“最后”“总而言之”这类连接词天然高频,模型在这些词上的概率长期偏高。贪心解码一路沿着最大概率走,很容易走出一段四平八稳、毫无信息量的话。这不是模型“笨”,而是解码规则把它逼成了复读机。概率分布的形态也加剧了这一点——多数情况下,下一个 token 的概率分布非常尖锐,集中在少数几个不会出错的候选上,而长尾里那些概率虽小但语义完全不同的 token 被直接无视。
生活里有个很像的场景:一场小组讨论,主持人规定每轮只让当前得票最高的人发言。得票第二、第三的人永远没有开口机会,就算他们脑子里有完全不同的好点子,也会被规则埋没。Brainstorming 类机制要做的事情,本质上就是改变这个“发言规则”。
1.2 发散生成到底在优化什么目标
常规生成追求的是“条件概率最大化”,也就是给定 prompt 之后,找出概率最高的那个续写。这在事实问答、代码补全、摘要提取这类任务里是对的,因为答案有明确的对错。但在创意生成、方案设计、命名建议、路线规划这些场景里,最高概率路径往往不是最有价值的路径——它只是最不会出错的路径。
所以发散生成把目标从“最大化概率”换成了“在合理约束下最大化多样性”。落到实现层面,就两个动作:一是引入随机采样,给低概率 token 一个出场机会;二是显式抑制重复,防止随机性让模型在同一个小范围里反复横跳。
我把一套完整的 Brainstorming 实现机制抽象成三段式:采样扰动—去重约束—择优收敛。
- 采样扰动:用随机采样取代贪心,让同一次 prompt 的多轮请求产生彼此不同的输出。
- 去重约束:通过重复惩罚、多样性指令等手段,保证输出之间的差异足够大,避免“看似生成了十条,其实是一条”。
- 择优收敛:最后用一个低温度调用或评分函数,从发散结果里挑出最合适的,或者把多条结果整合成最终答案。
这个三段式很像人类头脑风暴会议的流程:先让大家天马行空提想法,期间不许批评,最后再把想法归类、筛选、组合。模型侧不过是把这个流程用概率采样和文本后处理实现了一遍。
1.3 三个环节如何衔接成一个管线
实际工程里,采样扰动、去重约束、择优收敛不是三个孤立步骤,而是一条循环管线。我先说整体框架,后面几章会逐个展开细节。
第一步,用一个偏高的温度和一个较大的候选数量发起多路请求,得到 N 条初稿。第二步,对 N 条初稿做去重——既要在 token 层靠重复惩罚压制模型自身的复读倾向,又要在文本层用相似度计算剔除语义重复的候选。第三步,把去重后剩下的结果集中起来,用一个较低温度的调用让模型做评价、筛选、融合。这个三步管线也是后续给读者实操代码的基本结构。
2. 三大关键参数:Temperature、Top-p、Repetition Penalty 怎么配合
2.1 温度系数:把概率分布“摊开”或“捏尖”
温度系数是最直观、也最容易被误用的一个参数。它的数学本质是对 softmax 里的 logits 做一个缩放:实际计算时用的是 softmax(logit / T),而不是直接对概率调整。
当 T 大于 1 时,logits 被压缩,概率分布整体变平坦。原本概率只有 0.3 的低频 token,可能升到 0.12;原本概率 0.6 的高频 token,可能掉到 0.35。分布越平坦,采样时落到不同候选上的可能性越大,输出就越多样。当 T 小于 1 时,分布被捏尖,高低频之间的差距被拉大,几乎每次都落在最高概率的几个 token 上。T 趋近于 0 时,输出和贪心解码基本一致。
我实测下来有一个经验区间,可以给新手一个相对稳的起点:
| 场景 | 温度推荐 | 原因 |
|---|---|---|
| 创意文案、命名、故事续写 | 0.9 ~ 1.3 | 需要一定随机性,但不能彻底失控 |
| 产品方案、路线规划 | 0.7 ~ 1.0 | 保证结构完整,同时保留多方向可能 |
| 代码生成、数学推理 | 0.2 ~ 0.4 | 逻辑正确性优先,多样性是副作用 |
| 事实问答、摘要提取 | 0.1 ~ 0.3 | 尽量贴近标准答案 |
有一点容易踩坑:T=0 在大多数实现里不是真的除以 0,而是直接做 argmax,有的框架用 1e-7 之类的极小值近似。所以你在某些接口里设置 temperature=0,行为其实是确定性输出。
2.2 Top-p 核采样:动态裁剪候选池
温度把概率分布摊平以后,长尾里的 token 确实获救了,但也带来了新问题——有些 token 纯粹是噪声,概率只有 0.0001,它们混进输出只会让文本变得莫名其妙。核采样(Top-p)就是为了解决这个问题。
核采样的逻辑很巧妙:把所有 token 按概率从高到低排序,然后从最高位开始逐个累加概率,直到累计概率达到 p 这个阈值,就只保留这一批 token 作为候选池,其余全部砍掉。注意,这个候选池的大小是动态的。如果分布非常集中,可能只需要 5 个 token 就够到 90% 概率质量;如果分布被温度摊得很平,则可能需要几百个 token 才够。这一点比 Top-k 那种“固定取前 k 个”的方式更贴合实际分布形态。
我给新手的建议是:p 值设在 0.9 到 0.95 之间比较平衡。p=0.9 意味着候选池覆盖了 90% 的概率质量,既能保留主要可能性,又剔除明显离群的噪声。p=1.0 等于不做任何裁剪,这时候长尾噪声会跟着温度一起抬头。
温度 T 和核采样 p 是配合着用的:T 决定分布的形状,p 决定从形状里捞多大范围。只调 T 不管 p,输出可能粗糙;只调 p 不管 T,候选池形状没有变化,多样性上不去。
2.3 重复惩罚与多样性约束
温度保证“每次结果不同”,但不保证“同一次结果内部不重复”。有些模型在高温下会开始自我复读,同一个词反复出现,甚至在长文本里陷入局部循环。重复惩罚(Repetition Penalty)在 logits 层面做了干预:每当某个 token 已经生成过,就在它下一次出现的概率分上打一个折扣,通常是直接把它的 logits 除以一个惩罚系数。
惩罚系数大于 1 时,已出现的 token 被明显压制。我实测中常见取值范围是 1.0 到 1.15。超过 1.2 之后,中文文本的可读性会明显下降,因为模型为了避免重复,会刻意避开那些正常语境下该出现的常用词。
这里有一个中文场景特有的坑:中文里“的、了、是、在”这类功能词本身出现频率就极高,惩罚系数一高,模型会开始“绕着走”,宁可把句子写得拧巴也不肯再用这些词。所以中文发散生成时,重复惩罚宁可保守一点,把更多的去重工作交给后处理阶段的相似度计算。
除了重复惩罚,很多接口里还有 frequency penalty 和 presence penalty 这对概念。前者惩罚“出现频率高”的 token,后者惩罚“已经出现过”的 token。实操里我更常用 presence penalty:它只关心“有没有出现过”,不太在意出现次数,对语义多样性的促进更直接。
2.4 几个参数的配合经验
单看参数没意义,关键是组合。我自己跑文案类 Brainstorming 时最常用的组合是:temperature=1.1,top_p=0.92,presence_penalty=0.3,repetition_penalty=1.1。这个组合出来的文本既有多样性,大结构上也不会跑飞。而做代码补全类的“多方案探索”时,我会把 temperature 降到 0.3 左右,top_p 提到 0.95,因为代码场景里哪怕想得到不同写法,也必须保证每个候选是可运行的。
调参的时候我建议一次只动一个变量,用同样的 prompt 跑 8 到 10 次,观察输出差异。如果输出几乎一样,先加温度;如果输出开始胡言乱语,再收缩 top_p;如果输出重复率高,再小步加惩罚系数。不要同时把所有参数都推到极端,否则会得到一个“灵感四射但没法看”的结果。
3. 更强的发散手段:多路采样与视角切换
3.1 Best-of-N:用并行采样创造“候选池”
单次生成无论怎么调温,都只是一次采样。真正的 Brainstorming 机制需要有“多个人发言”的效果,这一步靠多路采样实现:同一个 prompt,同一个参数组合,连续请求 N 次,得到 N 条独立候选。这就是所谓的 Best-of-N 策略。
有人会问:既然温度和随机性已经让每次结果不同,这条路不就是多花几倍成本吗?确实要多花成本,但收益也是实打实的。我的经验是,生成 8 到 12 条候选之后,出现“惊喜内容”的概率会明显提升。原因在于:一次采样就像一个人抛硬币,结果随机但有限;多次采样相当于一群人各自抛硬币,总有人能抛出你最想要的那个面。如果预算允许,N=8 是性价比比较高的起点。
N 也不是越大越好。超过 15 条之后,新增候选里大部分是已有候选的变体,边际收益快速下降。而且后面还要对候选做去重和评分,N 越大,后处理的成本和延迟越高。
并行采样之后,需要有一个筛选环节。最简单的筛选是规则打分:长度是否合适、是否包含必要关键词、是否与已有候选中高度相似。稍复杂一点的做法是再用一次低温度模型调用,把所有候选集中起来,让模型充当“评委会”,挑出或整合出最合适的答案。
3.2 用提示词做“视角分裂”
参数层面的发散是概率层面的,提示词层面的发散则是语义层面的。两件事可以叠加,而且叠加效果很好。
“视角分裂”是我在实践中非常喜欢的一种提示词写法。思路是:不让模型以单一身份回答,而是让它同时扮演多个不同角色,要求各自从立场出发提出不同方向。举个例子,给产品设计任务时,我会让模型分别扮演设计师、工程师、市场运营和客服,各自提一套方案。这样做的好处是,四个角色的评价维度天然不同,提出的方向往往会落到完全不同的象限里。模型在扮演一个具体角色时,会调动与该角色相关的语料分布,这比单一身份更容易跳出思维惯性。
另一种写法是显式要求“方向多样性”:“请给出 5 个完全不同的方向,每个方向用一句话概括核心思路,方向之间不得重叠。”这听起来像一句废话提示,但实测很管用。模型对这类元指令的执行力相当好,它会主动避免同类内容重复出现。
还有一招是问题重塑:把一个大问题拆成多个小问题分别发散,再汇总。比如“为一个周末活动想 10 个点子”不如拆成“想 3 个室内活动点子、3 个户外活动点子、2 个适合亲子参与的点子、2 个预算极低的点子”。子问题的边界清晰以后,模型不容易在同一个思路里打转。
3.3 两阶段“发散—收敛”的流程设计
一个完整的 Brainstorming 生成器,至少要包含两阶段调用。
第一阶段是发散,参数偏高:temperature=1.1 左右,多路采样,不做太强硬的重复惩罚。这一阶段的目标只有一个:让候选之间尽量不同。第二阶段是收敛,参数调低:temperature=0.3 左右,把第一阶段产出的候选全部拼接成一个列表喂给模型,让它扮演“评审组长”,任务是先总结每个候选的核心观点,再分析各自的优缺点,最后给出一个融合后的最优方案。
这个两阶段流程是我在大量实践中摸索出来的。直接让模型一步到位给出“最佳方案”,它往往会变成一个平庸的常规答案。但先发散后收敛,模型在两个阶段用了两种不同的认知模式:发散时靠概率随机性探索空间,收敛时靠低温度做逻辑压缩。这有点像人类开会:头脑风暴阶段不许否定任何想法,汇总阶段再开始挑刺和整合。
需要提醒的是,第二阶段输入的 prompt 要把第一阶段的结果原样贴进去,并且明确告诉模型“这些是候选方案,不是最终答案”。否则模型可能误以为自己只需要评价某一条,导致整合不完整。
4. 实操:搭一个可复用的发散生成器
4.1 参数基线与场景对照
进入代码之前,先给一张我自己实测过的参数基线表,方便直接抄作业。
| 场景 | temperature | top_p | presence_penalty | repetition_penalty | 采样数 N |
|---|---|---|---|---|---|
| 通用创意点子 | 1.1 | 0.92 | 0.3 | 1.1 | 8 |
| 产品方案设计 | 0.8 | 0.9 | 0.2 | 1.05 | 6 |
| 命名/口号 | 1.25 | 0.95 | 0.4 | 1.15 | 10 |
| 活动策划 | 0.9 | 0.9 | 0.2 | 1.05 | 8 |
| 代码多方案 | 0.3 | 0.95 | 0.0 | 1.2 | 5 |
注意这只是起点,不是标准答案。不同模型对参数的敏感度差异很大,拿到一个新的对话模型接口时,先跑 2 到 3 条测试,看看默认输出的风格再微调。
4.2 核心代码实现
下面给一套完整可运行的 Python 伪代码。不同模型厂商的 SDK 命名会有差异,核心逻辑是通用的。
import hashlib from dataclasses import dataclass # 假设 client 是你的模型调用客户端,不同厂商 SDK 大同小异 client = get_llm_client() @dataclass class BrainstormConfig: temperature: float = 1.1 top_p: float = 0.92 presence_penalty: float = 0.3 repetition_penalty: float = 1.1 max_tokens: int = 500 n_samples: int = 8 def generate_one(prompt: str, cfg: BrainstormConfig) -> str: """单次采样,返回一条文本候选。""" resp = client.generate( prompt=prompt, temperature=cfg.temperature, top_p=cfg.top_p, presence_penalty=cfg.presence_penalty, repetition_penalty=cfg.repetition_penalty, max_tokens=cfg.max_tokens, ) return resp.text.strip() def fast_dedup(texts: list[str], threshold: float = 0.65) -> list[str]: """基于字符 n-gram 的重叠度做快速去重。""" def char_ngrams(text: str, n: int = 4) -> set: return {text[i:i+n] for i in range(len(text) - n + 1)} def overlap(a: str, b: str) -> float: if not a or not b: return 0.0 sa, sb = char_ngrams(a), char_ngrams(b) inter = len(sa & sb) union = len(sa | sb) return inter / union if union > 0 else 0.0 kept: list[str] = [] for t in texts: if all(overlap(t, k) < threshold for k in kept): kept.append(t) return kept def brainstorm(prompt: str, cfg: BrainstormConfig) -> dict: """完整的多路发散生成流程。""" candidates = [generate_one(prompt, cfg) for _ in range(cfg.n_samples)] deduped = fast_dedup(candidates) # 如果去重后太少,说明采样多样性不足,可以考虑小幅调高温度 if len(deduped) < 3: cfg.temperature += 0.1 deduped = fast_dedup( [generate_one(prompt, cfg) for _ in range(cfg.n_samples)] ) # 收敛阶段:低温度调用,让模型从候选中筛选、整合 review_prompt = ( "以下是针对同一个需求生成的候选方案,请先逐条总结核心观点," "再比较各自的优缺点,最后给出一个融合了各家长处的最终方案。\n\n" + "\n\n".join(f"候选{i+1}: {t}" for i, t in enumerate(deduped)) ) final_answer = client.generate( prompt=review_prompt, temperature=0.3, top_p=0.9, max_tokens=800, ).text.strip() return { "candidates": deduped, "final_answer": final_answer, }这套代码的亮点在 fast_dedup:它用字符 n-gram 的交并比判断两条文本的相似度。相比于逐字比较,它更接近语义层面的重叠检测。阈值 0.65 是我在中文场景下调出来的经验值,太高会留下大量近义重复候选,太低会误杀有用信息。
4.3 两套可以直接用的发散提示词模板
提示词是发散效果的另一半。我把自己常用的两套模板分享出来,一套偏通用创意,一套偏方案设计。
通用创意模板:
你是一个创意激发器。针对下面的需求,请给出 5 个完全不同的方向。 要求: 1. 每个方向用一句话说明核心思路; 2. 方向之间不得重叠,不得互相补充; 3. 允许大胆、反常识的思路,但必须有可执行性。 需求:{需求描述}这套模板的精髓在“不得互相补充”这几个字。模型如果只是被要求“给出建议”,天然倾向于写同一条主线的 5 个变体。明确禁止补充关系之后,它会主动往不同方向跳。
方案设计模板:
现在你是评审组长。针对下面的需求,请先给出 4 个完全不同的候选方案。 每个方案用 3 条要点说明:核心逻辑、所需资源、潜在风险。 候选方案之间必须来自不同的思路流派,禁止近义重复。 需求:{需求描述}这个模板把“发散”和“结构化”揉在了一起。每个方案限定 3 条要点,是为了防止模型在一个方案里展开太多细节,导致其他方案被压缩。先列候选,后面再单独调低温度让模型从中选优,效果非常稳定。
5. 常见问题与排查记录
5.1 症状、原因与解法速查
我把实际操作中遇到的高频问题整理成一张速查表,方便对照排查。
| 症状 | 可能原因 | 解决思路 |
|---|---|---|
| 多次生成结果几乎一模一样 | 温度过低或 top_p 过小 | 先升高温度到 1.0 以上,再适当放开 top_p |
| 结果千奇百怪但都不可用 | 温度过高且没有约束 | 降低温度,同时收紧 top_p 到 0.9 以内 |
| 同一段文本内部反复重复 | 重复惩罚不够 | 小幅增加 repetition_penalty,每次加 0.05 |
| 候选之间看似不同,核心思路相同 | 只做了参数层发散,没有提示词层发散 | 改用“视角分裂”或“多方向显式枚举”写法 |
| 收敛阶段模型没有整合,只挑了一条 | 第二阶段 prompt 里没交代清楚任务 | 明确要求“先总结、再比较、后融合” |
| 中文输出变得拗口、绕弯 | 重复惩罚太高,功能词被压制 | 把 repetition_penalty 降到 1.1 以下,去重交给后处理 |
5.2 三个值得单独展开的坑
第一个坑:温度调得很大,但 top_p 设得很小。这会让模型在连同候选项都凑不满的情况下强行换词,结果就是不相关词汇的堆砌。温度负责“摊开”,top_p 负责“圈定范围”,两者是互相制衡的关系,只动一边是不行的。
第二个坑:重复惩罚误伤中文功能词。我一开始做中文创意生成时,把 repetition_penalty 拉到 1.3,结果模型开始整段输出“机器味”极重的文本。后来我改成把惩罚控制在 1.1 左右,去重交给后处理的相似度过滤,效果立刻正常了。在中文场景里,文本层的去重比 logits 层的惩罚更安全。
第三个坑:用字面重合度判断语义重复。两句话可能一个用“实用”,一个用“好用”,字面完全不同但语义一样。单纯用 Jaccard 或编辑距离判重会漏掉大量近义重复。后来我改用 n-gram 重叠度搭配一个较小阈值,同时允许人工抽查,才算把这个问题压住。
5.3 怎么判断一组发散结果“好”还是“不好”
发散结果的评估不能只看多样性,还得看有用性。我给自己的指标是三个维度:
多样性可以用候选之间的平均重叠度来量化,重叠度越低越好。覆盖面看候选是否落在不同方向,这个往往要人眼判断,生成结束后把候选丢给一个低温度模型按“方向类型”归类,也很有效。有用性则看有多少候选可以直接用、改改能用、或者至少能启发后续思考。我通常要求三类占比不低于一半。
这些指标不用做得很重。偶尔跑一批自动评估,更多时候靠阅读手感判断。发散的底层逻辑是概率探索,本来就允许一定比例的无效产出,这是成本,也是必然。
6. 别把脑暴模式当成万能模式
6.1 什么任务适合发散,什么任务不该发散
最后想聊一个经常被忽略的问题:不是所有任务都适合开启 Brainstorming 模式。
事实问答、代码修复、参数排查、公文摘要这类任务,答案是收敛的,发散只会增加踩雷概率。有一次我做一段 JSON 格式的转换任务,顺手开了高温度,结果模型给我输出了一段充满创意的、完全不合法的 JSON 结构。从那以后,我给固定格式类任务设了一个硬性规则:temperature 不超过 0.3。
发散模式真正有价值的地方在:创意命名、文案构思、产品方向探索、活动策划、技术选型的前期调研、以及一切“没有标准答案,但需要多个视角”的任务。判断标准很简单:如果这个问题存在唯一正确答案,就不要用 Brainstorming;如果问题越开放越好,那就值得把温度和采样数拉起来。
6.2 我个人的一点体会
做这套发散生成器,最大的收获不是参数调得多好,而是理解了“随机性”和“约束”的关系。模型本身存储了无数种可能性,默认解码策略让它们中的绝大部分永远没有出场机会。Brainstorming 机制像一把扳手,把概率分布拧松一点,让更多可能性冒出来。但拧得太松又会变成胡言乱语,所以还要靠核采样、惩罚系数和收敛阶段的低温度调用把方向拉回来。
如果你只是往温度参数里填一个 1.5 就期待模型变“有创意”,大概率会失望。真正有效的发散是一个系统:参数层面要有采样扰动,提示词层面要有视角切换,后处理层面要有去重与收敛。把这条链路搭起来之后,模型才能真正像一场高质量的头脑风暴会——大家各说各话,最后还能落到一个靠谱的结论上。