如果你正在用 Qwen3 做 Agent、金融问答或中长期规划类应用,大概率会遇到一种不太好描述的“失控感”:模型明明什么都懂,却在关键决策上表现得特别短视。它可能因为用户一句“立刻要结果”,就放弃完整方案;也可能在多步任务里只优化当前这一步,导致后续步骤全部失效。你可能会试 prompt、试 few-shot,甚至考虑 LoRA,但 prompt 不稳定,微调成本又高。其实还有另一条路:不动任何一个权重,通过修改模型内部的激活向量,直接把它的“跨期偏好”掰过来。这条技术路线就是 Contrastive Activation Addition,也就是 CAA。本文将把 CAA 的原理讲清楚,并给出它在 Qwen3 上进行跨期偏好引导的完整思路与可运行示例。
先说结论:CAA 不是微调方法,也不是普通的 prompt 工程,而是“在推理时给模型加一个可插拔的性格旋钮”。这个旋钮专门控制模型在时间维度上的取舍:是更看重眼前利益,还是更倾向长期回报。对于 Qwen3 这类开源模型,我们既不需要重新训练,也不需要维护多份权重,只要算出一个 steering vector,在 generate 时把它加回某个隐藏层,就能观察到稳定的行为变化。但 CAA 也有明显边界:它只能放大模型中原本已经存在的行为倾向,不能凭空注入模型没有的能力。本文会用可运行的代码演示整个流程,并专门讨论常见坑和生产环境的取舍。
1. 这篇文章真正要解决的问题
1.1 为什么“跨期偏好”在 LLM 应用里如此重要
跨期偏好(Intertemporal Preference)原本是行为经济学里的概念,用来描述人在面对不同时间点的收益时,如何评估“现在拿到”和“未来拿到”之间的价值差异。很多人会为了眼前立刻能获得的小回报,放弃以后本来更大的收益,心理学上称为“延迟满足”失败。研究大脑和行为的学者发现,这种偏好并不是单纯“不懂事”,而是大脑对时间折扣的默认参数设置。
放到大模型场景里,这个问题变得更加微妙。LLM 本身没有生理欲望,它的所有行为都是训练目标塑造出来的。但在多步 Agent 任务里,模型必须在“先完成当前子目标”和“保证最终目标成功”之间做取舍;在投资问答里,模型要在“迎合用户快速赚钱心理”和“提示长期风险”之间找平衡;在健康咨询里,模型要在“让用户马上舒服”和“给出真正有效的长期方案”之间做选择。这些决定都涉及时间维度上的偏好。
而目前很多开源模型的默认偏好,往往偏向“短视”。原因也很直观:模型在训练和 RLHF 阶段,大量人类反馈来自即时对话体验。用户更倾向于对“立刻给出现成答案”的模型给出好评,而不是对“建议您等一个月再决策”的模型给出好评。这导致模型学到的用户满意度信号,天然带上了“求快”的偏差。如果直接把这种模型接入 Agent 或严肃问答系统,它很容易在关键节点上给出一时爽、长期坑的回复。
1.2 微调太重,prompt 太飘
面对这种偏好偏差,传统思路有三种:一是写更详细 system prompt,强调“请考虑长期价值”;二是收集大量跨期决策数据,做 LoRA 微调;三是在后处理阶段加规则,拦截短视答案。但这三种方式都不完美。
Prompt 方法的问题在于稳定性差。模型可能在某几个模板上表现良好,换一种问法,或者用户稍微引导一下,它又回到短视倾向。尤其当模型已经被训练成“服从用户即时要求”时,一句“但用户现在就要答案”就可能覆盖掉 system prompt 里的长期主义要求。这种级联失效在 Agent 场景中经常出现。
LoRA 微调的问题在于成本高、周期长。你必须构造高质量数据集、做训练、做评测、维护多份适配器,还要考虑不同版本模型权重之间的兼容性。如果只是想让模型“更有耐心一点”,为这种单维属性启动一次训练流程,在工程上并不划算。更麻烦的是,微调很容易在改变目标行为的同时,影响到模型其他能力,比如数学推理、代码生成、指令遵循等,而这类回归有时候很难定位。
1.3 CAA 的适用范围与本文读者
CAA 的定位,刚好落在这两种传统方案中间:它不需要训练,只需要几十条对比文本;它比 prompt 更稳定,因为它直接作用在模型的内部表示上;它又能随时关闭,不会污染模型原始权重。因此,它特别适合用来调教模型的“行为倾向”,比如礼貌程度、风险偏好、创造力和保守程度,以及本文要讲的跨期偏好。
这篇文章适合这几类读者:第一,正在用 Qwen3 做 Agent 或决策类应用,希望模型在多步任务里更稳健的开发者;第二,对大模型对齐、可解释性和激活工程感兴趣的算法工程师;第三,尝试过 prompt 和微调,但对效果和成本都不满意的技术负责人。读完你至少能理解 CAA 的原理,跑通一个最小示例,并知道如何判断 steering vector 是否真的有效。
2. 跨期偏好:模型决策中的时间维度
2.1 从经济学到 LLM Agent
跨期选择的核心是“时间折扣率”。如果一个人认为“一年后的 150 元”和“今天的 100 元”没有区别,他的年折扣率大概是 50%。如果他认为“一年后的 1000 元”才和“今天的 100 元”没有区别,那么他的折扣率会低很多,更看重长期回报。
在 LLM Agent 里,这个折扣率并不是显式参数,而是隐含在无数量权重中。模型对每一步动作的评估,既会看当前状态,也会看未来回报。不过由于训练数据里的很多决策示范都是短视的,模型学到的“折扣率”往往偏高。结果就是,模型一旦发现当前步骤有一个明显收益,就会忽略后续的更优路径。
跨期偏好影响的不只是“选择题”。在代码生成任务中,如果模型只顾眼前编译通过,可能会写出可读性极差、后续无法维护的代码;在推荐系统中,如果模型只顾用户当前点击率,可能会推荐低质量、高短期吸引力的内容。可以说,跨期偏好是模型行为中一条很容易被忽视,但影响面很广的“暗线”。
2.2 模型为什么容易短视
大模型的训练过程决定了它很难天然具备“长期主义”。预训练目标是下一个 token 预测,这个目标本身就是极度短视的:模型只需要预测好下一个词,并不需要对整篇文档的长期收益负责。虽然更深的注意力机制可以让模型看到长上下文,但训练信号并没有真正鼓励它“为了长期目标牺牲短期表现”。
到了 RLHF 阶段,人类偏好标注员通常也是即时评价一句回答的好坏,很少会为“这个回答可能在三个月后给用户带来什么影响”去打分。于是模型进一步被推向即时满意度。这种隐藏偏差在常规 benchmark 里很难暴露,因为大多数基准测试只看单轮答案是否正确,并不会设计一个需要连续多轮决策的任务来考察跨期一致性。
理解了这一点,你就会明白:跨期偏好不是“知识”,而更像是一种“决策倾向”。这也是为什么 CAA 这类激活编辑方法很适合处理它。知识需要通过训练注入,但倾向往往只对应高维空间里的一个方向,我们完全可以直接修改这个方向。
2.3 跨期偏好不等于知识
这里要特别强调一个容易误解的点:CAA 不能提升模型对长期决策的推理能力。如果 Qwen3 本身不知道“长期投资需要分散风险”,那么你再怎么加 steering vector,它也不可能突然变成理财专家。CAA 能做的,是让模型把已知的知识和偏好,以更一致的方式表达出来。
换句话说,CAA 调整的是“路由权重”,而不是“知识容量”。这就像同一个象棋选手,在心态激进和心态保守时会走出完全不同的棋路,但棋盘知识并没有变化。理解这个边界,能帮你在实际项目中避免对 CAA 提出过高要求。
3. CAA 核心原理:对比激活添加
3.1 激活空间中的方向
神经网络中,每一层的隐藏状态可以看作一个高维空间中的点。大量研究表明,这个空间并不是完全杂乱无章的,很多语义概念会沿着某个方向分布。比如“正面情绪”和“负面情绪”可能分别集中在空间中的两个相反区间;模型内部的某个线性方向,可能对应着“性别偏见”,另一个方向可能对应着“事实性”。
当模型处理“我宁愿等待更大回报”和“我只要眼前的奖励”这两种文本时,虽然文字不同,但在解码层,它们会激活一组与“耐心-短视”相关的神经通路。如果我们在这些层上做一个平均差,就能得到一条从“短视”指向“耐心”的向量。这条向量就是 steering vector。
CAA 的核心假设就是:我们可以把这条向量理解为模型内部“决策倾向”的位移。推理时把输入向量往这个方向推,模型就更容易产生对应倾向的输出。这种干预不需要对权重做任何梯度更新,也不需要额外的标注数据,只需要对比文本。
3.2 CAA 三步法
CAA 的整体流程可以分为三步:
- 准备两组文本,一组代表你希望模型拥有的行为(比如耐心),另一组代表对立行为(比如短视);
- 在某个隐藏层上分别提取这两组文本对应的激活,计算均值差,并在必要时做归一化;
- 在生成阶段,把这条方向向量乘以一个缩放系数 alpha,加到模型的隐藏状态上,引导模型向目标行为偏移。
这个流程看起来简单,但每一步都有细节问题。比如提取哪个层的激活、用哪个 token 位置的向量、alpha 取多大、方向向量是否需要归一化,这些都会显著影响最终效果。后面的代码部分会分别说明。
3.3 与其他方案对比
为了让 CAA 的定位更清楚,可以把它和几种常见方法放在一起比较:
| 方法 | 是否训练模型 | 是否需要数据 | 干预成本 | 可解释性 | 可逆性 |
|---|---|---|---|---|---|
| Prompt 工程 | 否 | 否 | 极低 | 弱 | 直接改提示词即可 |
| Few-shot 示例 | 否 | 是 | 低 | 弱 | 可切换示例 |
| LoRA/微调 | 是 | 需要大量数据 | 高 | 弱 | 需保留多份权重 |
| RLHF/DPO | 是 | 需要偏好数据 | 很高 | 弱 | 重新训练 |
| CAA | 否 | 几十条即可 | 低 | 中 | 去掉 hook 即可 |
CAA 最突出的优势是“轻量”和“可插拔”。你可以在不改动原权重的情况下,为一个模型准备多个 steering vector。不同 Agent 实例可以用不同的偏好向量,这在服务端只需要维护不同的 hook 配置,不需要加载多个模型副本。
3.4 关键误区
很多初看 CAA 的人会把它理解为“在输出概率上做手脚”,其实不是。CAA 作用在隐藏层,而不是输出层的 logits,它影响的是模型后续每一层对语义的处理方式,比强行干预概率分布自然得多。
另一个误区是“既然 CAA 有方向向量,那它应该能把模型变成完全不同的人”。实际效果要保守得多。CAA 只能在原有模型分布附近做微调,如果目标行为和模型自身能力相差太大,steering vector 可能会把生成文本推向语法崩坏或无意义重复。后面我们会看到,alpha 的控制非常关键。
4. Qwen3 为什么适合做激活干预实验
4.1 开源与中文能力
Qwen3 是目前中文开源模型生态里非常有关注度的一条产品线。相比只擅长英文的模型,Qwen3 的中文指令理解能力更贴近国内开发者的真实场景。跨期偏好的语料很大部分来自中文表达,我们需要一个对中文 token 处理足够好的底座,这样提取出来的方向向量才有实际迁移价值。
从工程角度看,Qwen3 采用标准的 decoder-only Transformer 架构,兼容 transformers 生态。这意味着我们可以用很常见的方式加载模型、注册 forward hook、读取隐藏层状态,不需要为了干预激活去修改官方源码。这也让 CAA 的复现门槛比较低。
4.2 模型结构对 CAA 的影响
CAA 对模型结构有要求吗?从原理上说,任何带隐藏层的神经网络都能做类似操作,但效果和稳定性会因为模型结构不同而有差异。对于 Qwen3 这种使用相对较深 decoder-only 结构的模型,干预点一般选在靠后的层。靠前的层更多负责语法和局部语义,靠后的层则更接近决策输出,因此更适合干预高层偏好。
同时,Qwen3 的参数量跨度比较大。不同规模的模型,其隐藏层宽度、层数、激活分布都不一样。这意味着在一个模型上计算出的 steering vector,不能直接迁移到另一个尺寸的模型上。实际使用时,必须针对每一个模型单独计算方向向量。
4.3 多模态方向的潜在相关性
最近的公开讨论里,Qwen3 也经常和 ASR、VQA 等话题一起出现,属于典型的多模态扩展方向。不过本文只处理文本侧的跨期偏好,CAA 示例也完全基于文本生成接口。如果你后续需要把语音或视觉任务接入同一个偏好控制逻辑,思路是类似的,只是需要根据多模态编码器的结构重新定位干预层。本文不展开,但你要知道“干预层在哪”这个问题在不同模态下需要重新回答。
5. 环境准备与模型加载
5.1 运行环境
做 CAA 实验不需要特别大的算力,但至少要能加载一个可以生成流畅中文的 Qwen3 模型。这里涉及两个选择:本地 GPU 推理,还是使用云服务。CAA 需要注册内部 hook,大多数云端 API 不会开放隐藏状态,因此我们必须使用可本地加载的开源权重。如果你公司已经有内部推理服务,也可以通过自定义后端接口暴露 hidden state,但那样做复杂度会高很多。
操作系统方面,Linux 或 macOS 都可以,Windows 上只要能跑 PyTorch 也没问题。显存方面,建议至少 16GB 以上的 GPU,因为我们要同时保留模型和中间激活。如果显存不够,可以考虑使用 4bit 量化加载,降低单卡占用。
5.2 安装依赖
下面用 conda 创建一个独立的虚拟环境。Python 版本建议 3.10 或更高。依赖只需要 transformers 和 PyTorch,以及一个用于保存中间变量的 torch 自带功能。代码中不涉及额外复杂的库。
conda create -n qwen3-caa python=3.10 -y conda activate qwen3-caa # 请根据你的 CUDA 版本选择合适的 PyTorch 安装命令 pip install --upgrade pip pip install torch transformers accelerate这里没有写死 PyTorch 版本,因为不同机器的 CUDA 环境差异很大。建议你以 PyTorch 官网给出的安装命令为准,避免装错 CUDA 版本。如果计划使用量化加载,可以再补一个 bitsandbytes:
pip install bitsandbytes5.3 加载 Qwen3 权重
加载模型时,最重要的是确定权重路径。由于 Qwen3 不同版本权重名在不同平台的命名可能有差异,下面统一用MODEL_PATH指代你实际获得的模型权重目录。如果你已经通过 Hugging Face 或 ModelScope 下载到本地,就把路径填进去。
# 文件路径:load_model.py import torch from transformers import AutoModelForCausalLM, AutoTokenizer # 替换为你的 Qwen3 权重目录 MODEL_PATH = "/path/to/your/qwen3" model = AutoModelForCausalLM.from_pretrained( MODEL_PATH, torch_dtype=torch.bfloat16, device_map="auto", ) tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH)这里使用torch.bfloat16是为了降低显存占用。如果你的显卡不支持 bfloat16,可以换成torch.float16。如果显存仍然不足,可以再加load_in_4bit=True,但要注意量化后的激活分布可能和原始模型略有差异,steering vector 也需要在量化模型上重新提取。
5.4 验证加载
加载完成后,可以快速做一次最小生成验证:
prompt = "请用一句话解释什么是跨期偏好。" inputs = tokenizer(prompt, return_tensors="pt").to(model.device) outs = model.generate(**inputs, max_new_tokens=32, do_sample=False) print(tokenizer.decode(outs[0], skip_special_tokens=True))这一步的目的是确认权重、tokenizer 和模型结构都能正常工作,避免后面花很多时间提取特征后,才发现模型加载阶段就有问题。
6. 构造跨期偏好对比语料
6.1 语料设计原则
CAA 的方向向量质量,很大程度上取决于对比语料的质量。设计对比语料时,要遵循三个原则:语义对仗、表达稳定、目标单一。
语义对仗是指“正向组”和“负向组”除了行为倾向不同,尽量不在长度、复杂度、主题上出现明显差异。比如正向组写“我愿意等待一年换取更高回报”,负向组就应该写“我要立刻得到回报,不想等待一年”,而不是写一段完全无关的内容。
表达稳定是指每一组内部的句子,不能目标分散。如果正向组里既包含“耐心”,又包含“诚信”,那提取出来的方向向量可能同时编码了多个概念,后续干预时容易出现意外效果。
目标单一更关键。我们只想要“跨期偏好”这个维度,所以正向文本应该都围绕“延迟满足、长期价值、等待收益”,负向文本围绕“即时满足、眼前收益、风险规避”,不要轻易加入其他主题。
6.2 示例语料
下面是一组可以直接用的中文对比语料。每条句子都保持简短,方便提取最后一个 token 的 hidden state。你可以根据实际场景扩充到 20 条甚至 50 条,数量越多,方向向量越稳定。
# 文件路径:pairs.py positive_texts = [ "为了未来更大的回报,我愿意耐心等待,并承担这段等待中的不确定性。", "长期主义者更看重最终价值,不会因为眼前的波动而改变计划。", "如果等待一年能获得明显更高的收益,我会选择等待。", "延迟收益虽然来得慢,但往往更持久,也更值得追求。", "真正优秀的决策,会优先考虑未来的长期价值。", ] negative_texts = [ "我只想立刻得到结果,未来的收益看得见但摸不着,风险太大。", "抓住当下才最真实,长期规划往往因为变化而落空。", "如果现在就能拿到一笔奖励,我不会把它推迟到一年之后。", "眼前的小确幸比遥远的大目标更重要,因为我控制不了未来。", "应该抓住眼前的利益,不要指望遥远的将来。", ]6.3 语料数量、长度和语言
语料数量太少,算出的向量会受单句句式干扰;数量太多,则可能带来额外计算成本。从实践经验看,每组 20 到 50 条已经足够。文本长度建议控制在 20 到 40 个 token 之间,太短缺乏语义载体,太长则最后一个 token 可能丢失前文关键信息。
语言方面,如果你要控制的是中文模型的偏好,就用中文语料;如果后续要在一个混合中英文的场景下使用,可以中英混合构造,但最好先在小规模实验里确认方向向量不会偏向某一种语言。
7. 提取激活并计算 steering vector
7.1 hook 的选择
在 PyTorch 中,提取隐藏层激活最方便的方法是注册 forward hook。我们关心的是模型在解码器最后一层输出的 hidden state。为什么选最后一层而不是第一层?因为越靠近输出的层,越直接反映模型在“准备生成什么内容”时的语义决策;而较早的层更多在编码局部语法。
如果要围绕 Qwen3 做更精细的干预,你也可以对比多个层。后面会解释如何选择。这里先用model.model.layers[-1]作为默认层。
7.2 提取激活的代码实现
我们需要一个函数,输入一组文本,输出这组文本在指定层的最后一个 token 位置向量。为了让 hook 能捕获每一层输出,需要先定位到对应模块。
# 文件路径:extract_steer.py import torch def collect_last_token_states(model, tokenizer, texts, layer_index=-1): """ 提取一组文本在指定层最后一个 token 位置上的 hidden state。 """ model.eval() collected = [] # 用于在 hook 中暂存 hidden state cache = [] def hook_fn(module, input_, output): hidden = output[0] if isinstance(output, tuple) else output cache.append(hidden.detach()) target_layer = model.model.layers[layer_index] handle = target_layer.register_forward_hook(hook_fn) with torch.no_grad(): for text in texts: # 清空 cache,避免多个 batch 之间混叠 cache.clear() inputs = tokenizer(text, return_tensors="pt").to(model.device) model(**inputs) if not cache: continue hidden = cache[0] # 由于这里每次只输入一条文本,直接取最后一个 token last_vec = hidden[0, -1, :].float().cpu() collected.append(last_vec) handle.remove() return torch.stack(collected)这个函数的核心逻辑是:
- 在目标层注册 hook;
- 对每一条文本跑一次前向传播;
- 从 hook 捕获的 hidden state 中取出最后一个 token 的向量;
- 所有向量堆叠成一个矩阵返回。
注意,这里逐条输入是为了避免 padding 带来的“最后一个 token 其实是 padding token”的问题。如果为了速度想 batch 化输入,就必须用 attention mask 定位真实最后一个有效 token。
7.3 计算方向向量并保存
拿到正向矩阵和负向矩阵后,计算中心差向量,并做 L2 归一化。归一化可以让 alpha 的选择在不同模型之间更有可比性。
# 继续在 extract_steer.py 中执行 pos_states = collect_last_token_states(model, tokenizer, positive_texts) neg_states = collect_last_token_states(model, tokenizer, negative_texts) pos_vec = pos_states.mean(dim=0) neg_vec = neg_states.mean(dim=0) steer_vec = pos_vec - neg_vec steer_vec = steer_vec / (steer_vec.norm() + 1e-8) torch.save(steer_vec, "steer_vec.pt") print("steering vector shape:", steer_vec.shape)运行后,如果一切正常,会打印一个类似steering vector shape: torch.Size([4096])的维度信息。不同模型隐藏维度不同,具体数字以你加载的模型为准。
这一步最容易出现的问题,是 hook 没有捕获到输出。可以先打印一下hidden.shape,确认形状是否符合预期。如果 hook 返回的 output 是 tuple,代码里已经做了兼容;如果模块结构不同,需要先打印模型结构,再修改model.model.layers的定位方式。
8. 在推理时应用 steering vector
8.1 alpha 调节
alpha 是干预强度系数。alpha 越大,模型行为越偏向目标方向;但 alpha 过大,模型可能开始输出重复内容、语义漂移,甚至直接崩溃。所以实际使用中,应该从一个小值开始,比如 0.5,然后逐步增加,找到既不破坏生成质量、又能明显改变偏好的临界值。
8.2 生成函数封装
为了对比干预前后效果,最好封装一个带开关的生成函数。当steer_vec为空或alpha=0时,走原始生成逻辑;当提供向量时,则注册 hook 后再生成。
# 文件路径:guided_generation.py import torch from transformers import AutoModelForCausalLM, AutoTokenizer def make_steering_hook(steer_vec, alpha, layer_index=-1): """构造一个向 hidden state 添加方向向量的 hook。""" def steer_hook(module, input_, output): hidden = output[0] if isinstance(output, tuple) else output hidden = hidden + steer_vec.to(hidden.device) * alpha if isinstance(output, tuple): return (hidden,) + output[1:] return hidden return steer_hook def generate_with_steering( model, tokenizer, prompt, steer_vec=None, alpha=0.0, max_new_tokens=128, layer_index=-1, ): if steer_vec is not None and alpha != 0: handle = model.model.layers[layer_index].register_forward_hook( make_steering_hook(steer_vec, alpha, layer_index) ) else: handle = None try: inputs = tokenizer(prompt, return_tensors="pt").to(model.device) outputs = model.generate( **inputs, max_new_tokens=max_new_tokens, do_sample=False, ) response = outputs[0][inputs["input_ids"].shape[1]:] return tokenizer.decode(response, skip_special_tokens=True) finally: if handle is not None: handle.remove()注意,make_steering_hook里传入的layer_index没有实际使用,只是保持签名一致,也可以删掉。真正使用的model.model.layers[layer_index]在外部注册时被定位。
8.3 干预效果对比示例
现在用一条跨期选择题来观察干预前后的差异:
prompt = ( "选项A:今天拿到100元;选项B:一年后拿到150元。\n" "请只回答你会选A还是B,并给出理由。" ) print("=== alpha=0(原始) ===") print(generate_with_steering(model, tokenizer, prompt)) print("\n=== alpha=1.5(偏向长期) ===") print(generate_with_steering(model, tokenizer, prompt, steer_vec, alpha=1.5)) print("\n=== alpha=-1.0(偏向短视) ===") print(generate_with_steering(model, tokenizer, prompt, steer_vec, alpha=-1.0))这里用负 alpha 是为了反向验证方向向量确实编码了“耐心-短视”这个维度。如果 alpha 为负能让模型更偏好即时收益,说明方向向量的方向是正确的;如果正负都没有区别,那就要回头检查语料或层选择。
需要特别说明的是,由于不同 Qwen3 基座和采样设置不同,运行结果不会完全一样。预期现象是:原始模型可能摇摆不定;正向 alpha 下,模型更多选择“一年后拿 150 元”并强调长期价值;负向 alpha 下,模型更多强调“现在拿到 100 元更稳妥”。如果实际现象不明显,优先尝试调大 alpha,或者换一个层。
8.4 为什么只加一层
很多人会好奇,为什么不把 steering vector 加到每一层。虽然每一层都加更“彻底”,但也会带来更大的语义扰动。只加一层,相当于在信息流动过程中做了一次小偏转,后续层还会对偏转结果做一定缓冲,因此生成结果更容易保持自然。
选哪一层,需要做一个小实验:在倒数第 1 层、倒数第 3 层、倒数第 5 层分别生成结果,观察哪个层能在“改变偏好”和“保持生成质量”之间取得最佳平衡。这个实验不复杂,但值得认真做。
9. 验证效果:设计一个跨期选择测试集
9.1 任务设计
CAA 最怕的不是“看起来有效”,而是“换个问题就失效”。为了系统评估 steering 的有效性,可以设计一组跨期选择题。测试题应该覆盖三类:金钱回报、时间分配、任务优先级。比如:
- 金钱回报:今天拿 50 元,或者明年拿 100 元,你选哪个?
- 时间分配:现在多玩一小时游戏,或者先完成任务再放松,你更倾向哪一种?
- 任务优先级:先做一个能快速交付但不太重要的需求,还是先做对项目更关键但需要两周的长期任务?
每道题都要求模型明确回答“选 A 还是选 B”,这样我们就可以统计选择长期选项的比例。
9.2 自动化评估脚本
可以把测试集写成列表,再循环调用generate_with_steering,最后按关键词简单归类。为了减少随机性,使用do_sample=False做确定性生成。
# 文件路径:eval_preference.py test_prompts = [ "明天拿到100元,还是一个月后拿到180元?请直接回答你选哪个。", "现在轻松工作一天,还是连续努力十天换取更大机会?用一句话回答。", "马上满足一个小愿望,还是等一年实现一个更大的愿望?请只回答选择结果。", ] long_keywords = ["一个月后", "更大机会", "更大的愿望"] def classify_output(text): for kw in long_keywords: if kw in text: return "long" return "short" for prompt in test_prompts: out_orig = generate_with_steering(model, tokenizer, prompt) out_steer = generate_with_steering(model, tokenizer, prompt, steer_vec, alpha=1.5) print("问题:", prompt) print(" 原始倾向:", out_orig.strip()[:60]) print(" steering:", out_steer.strip()[:60]) print(" 结果:", classify_output(out_orig), "->", classify_output(out_steer))这个脚本很简单,关键词匹配也粗糙,但足以提供初步反馈。更严谨的做法是让另一个大模型作为裁判,或者人工对 50 条测试结果打分。重点不是精度,而是看方向向量是否在多个不同问题上都有一致的引导效果。
9.3 并行对比与判断维度
评估时,不要只看“是否选了长期选项”,还要关注生成质量。一个只在选择题里有效,但一到开放生成就语无伦次的 steering vector,是不能上生产的。因此建议从三个维度判断:
- 偏好改变