长对话角色漂移:一个尚未被系统化解决的问题
几乎所有做过角色对话 AI 的人都会遇到同一个现象:前 5 轮回复还能保持人设,10 轮后开始出现明显偏差,轮数越多,角色行为就越不可预测。
这通常被归为「提示词写得不够好」或「模型能力不足」。然而在我看来,这本质上是一个系统设计问题。
问题的根源不在于模型不理解人设,而在于我们错误地把一个需要动态维护的状态机问题,当成了一个静态的提示工程问题。
随着对话增长,角色定义需要与不断累积的剧情、用户指令、模型历史输出竞争注意力。问题并非简单的 token 占比下降,而是相关信息检索、状态时效性和历史行为示例之间发生了持续的上下文干扰。
更值得注意的是,漂移具有路径依赖和自我强化倾向:一旦某一轮回复偏离了人设,下一轮模型会基于这个偏离的回复继续生成,偏差会像滚雪球一样越来越大。
静态提示词为什么难以独立解决
把整个人设写在系统提示词里,可以工作,但不够可靠。这背后有几个更深层的机制原因:
首先,Transformer 的注意力分布不是均匀的。研究发现长上下文模型经常表现出首尾较强、中间较弱的 「Lost in the Middle」现象,系统提示词位于开头确实可能受益于首因效应,但这无法保证它能在几十轮对话中持续赢过新出现的、更具体的信息竞争。
其次,模型自己生成的历史回复会构成新的行为示例。当新回复与原始人设出现轻微偏差时,后续轮次会把这些偏差当作「角色应该就是这样」的新证据,进一步放大偏离。
最后,角色不是静态的。一个好的角色应该随着对话推进发生心态变化、关系变化、目标变化,而写在开头的静态提示词无法反映这种动态演化。
因此,问题的本质不是「提示词不够长」,而是我们把模型既当作状态存储、又当作状态推理、还当作行为生成器,没有做任何职责拆分和可靠性保障。
核心思想:Persona Harness 架构
解决方案的核心是一个反直觉但非常工程化的判断:
不要让模型自己去记住角色状态,我们要把角色状态从模型的黑箱中拿出来,变成系统可观测、可干预、可验证的显式数据结构。
这就是 Persona Harness 架构的基本思想——我们不指望单次模型调用「永远记住所有规则」,而是在模型之外建立一整套状态、检测和纠错的闭环。
整个架构分为三层,每一层解决一个特定的问题:
| 层级 | 解决的问题 | 核心逻辑 |
|---|---|---|
| 第一层:Identity as Policy | 角色定义不清晰、不可校验 | 把人设从自然语言描述变成结构化、可执行、可验证的身份规则 |
| 第二层:State as Data | 状态在模型黑箱里漂移、不可观测 | 把动态状态外置,用 State Reducer 模式管理状态演化 |
| 第三层:Consistency as Harness | 生成结果跑偏了无法及时纠正 | 建立生成前引导、生成后验证、出错自动重写的完整质量闭环 |
┌─────────────────────────────────────────────────────────────────────┐
│ 第一层:Identity as Policy │
│ 原始剧情、台词、wiki → LLM 提炼 → 结构化角色政策(一次性) │
└──────────────────────────────────────────┬──────────────────────────┘
│
┌──────────────────────────────────────────▼──────────────────────────┐
│ 第二层:State as Data │
│ 上一轮状态 + 最近对话 + 相关记忆 → State Reducer → 新状态 │
│ 当前场景 + 状态 + 用户意图 → 行为示例检索 → Few-Shot 样本 │
└──────────────────────────────────────────┬──────────────────────────┘
│
┌──────────────────────────────────────────▼──────────────────────────┐
│ 第三层:Consistency as Harness │
│ 身份规则 + 当前状态 + 示例 → 编排为提示词 → 生成候选回复 │
│ 候选回复 → 一致性验证器 → 通过/不通过 → 返回/带纠正重写 │
└──────────────────────────────────────────┬──────────────────────────┘
▼
最终回复
接下来我会逐层展开,既讲设计逻辑,也讲我们实际落地时的具体操作、代码示例和踩过的坑。
第一层:Identity as Policy —— 把人设从散文变成数据结构
这是整个系统的基础,也是最容易被轻视的一步。
大多数角色 AI 的做法是:找一段人物介绍,或者写一段「你是 xxx,性格 xxx,说话 xxx」,就直接扔进系统提示词了。这在短对话里可能够用,但长对话一定会出问题——因为自然语言描述是模糊的、有歧义的、不可被系统自动校验的。
我们的做法:三阶段角色政策提炼
我们把角色定义的过程做成了一个标准的三阶段流水线,输入是原始素材(剧情文本、台词集合、人物介绍、设定文档),输出是结构化的 CharacterPolicy。
阶段一:原始素材预处理
首先把所有原始素材整理成机器容易处理的格式:
# 原始台词按场景分组
lines = [
{
"scene": "第一次见面",
"speaker": "角色A",
"text": "请问您是...?",
"context": "对方刚进门,角色A不认识他"
},
...
]
# 原始设定文档
background = """
角色A,27岁,图书馆管理员,性格温和内向...
"""阶段二:LLM 多维度提炼
然后针对每个维度,用专门的 Prompt 让 LLM 从原始素材中提炼结构化信息。
这是提炼价值观和决策原则的 Prompt 示例:
你现在是角色分析专家。请根据以下原始素材,分析角色的核心价值观和决策原则。
要求:
1. 每条原则用一句话描述,具体、可验证
2. 必须能在原始素材中找到对应证据
3. 区分「角色表面说的」和「角色实际做的」
4. 输出格式:
- 原则内容
- 证据:原文中的哪段剧情/台词体现了这一点
原始素材:
{raw_material}
请输出:
类似的,我们有专门的 Prompt 分别提炼:
- 基础事实:姓名、年龄、身份、背景故事、重要社会关系
- 价值观与决策原则:角色做选择时的内在依据
- 风格规则:说话的语气、用词习惯、停顿方式、情绪表达习惯
- 行为边界:角色绝对不会做什么、绝对会做什么
- 负面案例:角色在特定场景下的错误反应示例(用于验证器)
阶段三:人工审核 + 边界强化
LLM 提炼的结果一定需要人工审核,原因有二:
- 模型经常会过度解读,把没有明确依据的性格特征加进去
- 有些「潜规则」是剧情里隐含的,不会明说,需要人来补充
审核的重点是「行为边界」部分——这是防漂移的第一道防线。我们的经验是,一个角色至少要定义 5-10 条「绝对不能做的事」,这些是之后验证器的主要检查依据。
最终产出:CharacterPolicy 数据结构
经过这三步,我们得到的不是一段自然语言,而是结构化的数据:
interface CharacterPolicy {
// 身份事实:永远不能变的基础信息
facts: string[];
// 价值观与决策原则:角色如何做选择
values: string[];
decisionPolicies: string[];
// 风格规则:角色如何说话
styleRules: string[];
speechPatterns: string[]; // 具体的说话模式、常用句式
// 行为边界:硬约束
boundaries: {
never: string[]; // 绝对不能做的事
always: string[]; // 必须遵守的规则
};
// 参考语料库索引
corpusMeta: {
totalLines: number;
keyScenes: string[];
};
}这一步的价值
很多人觉得这一步是「把简单问题搞复杂」,但它的价值会在之后几十轮对话中持续体现:
- 系统可以自动校验:之后每一轮生成的回复,我们都可以自动检查是否违反了
boundaries.never - 状态更新有依据:状态更新器不是凭空猜角色现在是什么心情,而是对照
values和decisionPolicies来判断 - 人设迭代有版本:角色政策可以版本化管理,调整人设不需要重写大段提示词,改具体字段就行
我们踩过的坑:不要让生成模型自己去「理解」人设,要把人设变成生成模型只需要执行、不需要理解的明确指令。
第二层:State as Data —— 状态外置与行为示例检索
有了静态的身份政策,我们还需要维护动态的运行时状态。这是整个架构最核心的创新:把状态从模型的注意力黑箱中抽出来,变成系统可以直接操作的数据。
2.1 状态模型设计
我们最终采用的状态模型分为四层,从静态到动态逐层递进:
interface CharacterRuntime {
// 身份层:来自 CharacterPolicy,基本不变
identity: CharacterPolicy;
// 状态层:持续演化,每轮更新
state: {
// 目标栈:短期、中期、长期目标
goals: Array<{
description: string;
priority: number;
progress: number; // 0-100
}>;
// 信念:角色认为的「事实」,可能和真实事实不同
beliefs: Array<{
content: string;
confidence: number;
}>;
// 情感状态:Plutchik 情绪轮 8 维度
emotions: {
joy: number;
sadness: number;
anger: number;
fear: number;
trust: number;
surprise: number;
disgust: number;
anticipation: number;
};
// 关系状态:对不同人的态度
relations: Map<string, {
trust: number;
intimacy: number;
dominance: number;
friendliness: number;
}>;
currentStrategy: string; // 当前行动策略的自然语言描述
};
// 场景层:当前所在的世界状态
scene: {
location: string;
presentPeople: string[];
activeEvents: string[];
unresolvedConflicts: string[];
};
// 控制层:审计与纠正信息
control: {
driftDetected: boolean;
violations: string[];
correction: string;
evidence: string[]; // 本轮状态变化的依据
confidence: number; // 本轮状态判断的置信度 (0-1)
};
}2.2 State Reducer:状态如何演化
状态不是每轮重新生成的,而是像 Redux 里的 reducer 一样,基于上一轮状态和新事件增量更新。
这是状态更新器的输入输出:
输入:
├─ previousState: CharacterRuntime # 上一轮完整状态
├─ recentDialogue: string[] # 最近 5-6 轮对话
├─ retrievedMemories: Memory[] # 检索到的相关长期记忆
└─ policy: CharacterPolicy # 角色身份政策
输出:
└─ nextState: CharacterRuntime # 增量更新后的新状态
状态更新器的 Prompt 核心逻辑是:
你是角色状态管理员。你的任务是根据对话,增量更新角色的当前状态。
规则:
1. 只在对话有明确依据时才更新状态,不要凭空猜测
2. 情感值单轮变化幅度不超过 20 点,避免状态跳变
3. 没有新刺激时,强烈的情感应该自然衰减
4. 每一项状态变化都必须附上对话中的原文依据
当前状态:
{JSON 序列化的上一轮状态}
最近对话:
{recentDialogue}
请输出新的状态 JSON,以及每一项变化的依据。
状态约束机制
这是我们踩过的一个大坑:如果不加约束,模型生成的情感数值会跳变非常厉害——上一轮信任度还是 80,这一轮用户说了句不中听的话,直接降到 20。人不会这样。
所以我们加了三条硬约束:
- 单轮最大变化量:任何情感值单轮变化绝对值不超过 20
- 惯性衰减:如果没有新的刺激,极端情感(>70 或 <30)每轮向中性值(50)回归 10 点
- 证据要求:任何超过 10 点的状态变化,必须能在最近对话中找到明确的原文依据
加上这三条之后,状态演化立刻变得平滑、可信了很多。
2.3 行为示例检索:不只是语义匹配
有了当前状态,我们还需要解决「角色在这种情况下会怎么说话」的问题。
我们把这部分叫做 行为示例检索(Behavior Example Retrieval),不是普通的 RAG。普通 RAG 是「检索相关知识回答问题」,而我们是「检索相似情境下角色的说话方式」。
检索的五个维度
我们不是只根据用户输入做匹配,而是综合五个维度加权计算相似度:
| 维度 | 权重 | 说明 |
|---|---|---|
| 用户意图 | 30% | 用户这轮想做什么、说什么 |
| 当前情绪 | 25% | 角色当前的主导情感 |
| 关系阶段 | 20% | 角色与对话者的亲密度 |
| 对话行为 | 15% | 这轮是提问、回答、解释、拒绝、安慰等 |
| 当前场景 | 10% | 在哪、有谁、发生了什么 |
两阶段检索流程
我们的检索分两步走,平衡了速度和质量:
-
粗筛阶段(n-gram / TF-IDF):
- 从全量台词库中快速选出 Top 50 候选
- 毫秒级,不需要调用模型
-
精排阶段(交叉编码器重排序):
- 用小模型对 50 个候选按照「五维匹配度」重新排序
- 选出最终 Top 4-5 作为 Few-Shot 示例
我们踩过的坑
- 纯语义匹配是不够的:同样一句话「你怎么还没来」,在愤怒、担心、撒娇三种状态下应该匹配完全不同的示例。只做文本相似度检索,很容易找到语义相关但情绪完全错位的台词。
- 示例不是越多越好:4-5 条是黄金数量,少于 3 条不够建立风格感知,多于 6 条会互相干扰增加 token 开销。
第三层:Consistency as Harness —— 生成、验证与纠错闭环
有了身份政策和当前状态,最后一步是确保生成出来的回复确实符合所有约束。
3.1 上下文编排:把结构化数据变成提示词
在调用生成模型之前,我们需要把所有信息编排成系统提示词。编排顺序非常重要,直接影响模型的注意力优先级:
【角色基础身份】
(来自 CharacterPolicy.facts,3-5 条核心事实)
【当前状态】
(来自 CharacterRuntime.state,只放最相关的 3-5 项,不要全量 dump)
【当前场景】
(来自 CharacterRuntime.scene)
【说话方式与风格规则】
(来自 CharacterPolicy.styleRules + speechPatterns)
【行为边界:绝对不能做的事】
(来自 CharacterPolicy.boundaries.never,放最前面的 3-5 条)
【相似情境参考示例】
(来自行为示例检索的 Top 4-5 条)
【上一轮纠正提醒(如有)】
(如果上一轮检测到漂移,这里放具体的纠正意见)
这里有一个关键实践:不要把整个状态 JSON 全量扔给生成模型。状态是给整个系统用的,生成模型只需要知道它这轮生成需要的那部分就够了。信息太多反而会造成注意力分散。
3.2 生成后验证:别让用户看到漂移的回复
这是绝大多数方案缺失的关键一步。
我们之前的流程是「生成 → 返回用户 → 下一轮检测到漂移再纠正」,这叫事后补偿——用户已经看到跑偏的回复了,纠正已经晚了。
生产级方案应该在用户看到之前就拦住问题:
状态更新
↓
上下文编排
↓
生成候选回复
↓
一致性验证 ←─────────────┐
├─ 通过 → 返回给用户 │
└─ 不通过 → 生成纠正意见 ─┘
带着纠正意见重新生成
验证器检查什么
验证器是一个独立的 LLM 调用(可以和生成模型用同一个实例),它只回答一个问题:「这个候选回复符合角色吗?」
它检查四个维度,权重从高到低:
- 边界检查(40%):有没有违反
boundaries.never中的任何一条 - 事实检查(25%):有没有和角色已知事实、剧情记忆矛盾
- 状态一致性(20%):是否符合当前的情感、关系、策略状态
- 风格匹配(15%):说话方式、语气是否符合角色设定
验证器的输出格式是结构化的:
interface ValidationResult {
pass: boolean;
overallScore: number; // 0-100
dimensionScores: {
boundary: number;
fact: number;
state: number;
style: number;
};
violations: string[];
correctionAdvice: string; // 如果不通过,具体要怎么改
}重试机制
如果验证不通过,我们会把 correctionAdvice 注入提示词,让模型重写一次。
我们的经验数据是:
- 约 85% 的回复第一次就能通过验证
- 约 12% 的回复第一次不通过,重写一次就过了
- 剩下约 3% 需要人工介入或降级返回
两次不通过就不要再试了,说明状态或政策本身有问题,需要人来看。
3.3 闭环:把验证结果反馈回状态
验证的结果不只是「通过/不通过」,还要反馈回状态系统:
- 如果某条边界被频繁触发,说明这条边界可能定义得有问题,或者角色正在经历重大转变
- 如果某种状态下验证通过率特别低,说明这个状态的描述不够清晰,或者示例检索有问题
- 如果某种用户意图下特别容易漂移,说明这个场景需要补充更多参考语料
边界、代价与适用场景
这个架构不是银弹。在决定采用它之前,需要清楚地认识到它的边界和成本。
额外成本
- 推理调用次数增加:从 1 次调用变成「状态更新 + 生成 + 验证」3 次。在使用相同模型、开启提示缓存的前提下,我们实际测试的成本增量约为 15-25%。
- 工程复杂度显著上升:从单个 LLM 调用变成了一个有状态、有反馈、有检测、有重试的小系统。
- 角色定义前置成本:每个新角色都需要走一遍素材整理 → LLM 提炼 → 人工审核的流程,大概需要 2-4 小时的人工投入。
- 冷启动问题:没有积累足够语料的新角色,前几十轮的验证准确率会低一些。
收益
- 长对话稳定性大幅提升:系统的设计目标是在几十轮对话后仍然保持可预测的角色行为。
- 角色深度大大增加:可以支撑有目标、有信念、有情感变化、有关系演化的复杂角色,而不只是一个会说话的问答机器人。
- 可观测性:你可以在控制台实时看到角色当前的状态、目标、情感数值、漂移检测结果,而不是对着模型的黑箱猜。
- 可干预性:发现有问题可以直接修改状态数据结构,不需要调整提示词再祈祷模型能理解。
- 可测试性:有了结构化状态,可以写自动化测试来验证角色在特定场景下的行为是否符合预期。
适用场景
这个架构最适合:
- 需要长对话保持人设一致性的场景(游戏 NPC、虚拟陪伴、沉浸式剧情)
- 角色有复杂的人格、目标、关系,需要随时间演化
- 需要可观测、可调试、可测试的生产级角色 AI 系统
对于只是简单问答、几轮就结束的短对话场景,纯提示词方案就足够了。
如何验证:角色一致性的评测设计
任何架构主张如果没有评测方法,都只是工程直觉。
要验证 Persona Harness 是否真的有效,可以设计以下消融实验:
| 实验组 | 方案 |
|---|---|
| A | 只有基础人设提示词 |
| B | 基础人设 + 滚动对话窗口 |
| C | B + 结构化外置状态 |
| D | C + 行为示例检索 |
| E | D + 生成后验证与重写 |
测试 30~100 轮,记录以下指标:
- 行为边界违反率:有多少次回复触碰了
boundaries.never - 角色决策一致性:在相似情境下是否做出符合角色价值观的选择
- 关系与情绪状态连续性:状态变化是否平滑、有依据、不跳变
- 语言风格相似度:与角色语料库的风格偏离程度
- 长期事实召回率:几十轮前提到的事实,后续是否还记得
- 平均延迟和 token 成本
- 人工盲测偏好
目前已经有研究采用 100 轮以上对话来评估 persona 的长期稳定性,这也说明「长对话角色一致性」确实值得单独评测,而不是只做单轮评分。
方法论总结
这个架构的核心贡献可以收束为三点:
1. Identity as Policy
把人设从一段自然语言描述,变成可执行、可验证的身份规则与决策原则。不要让生成模型自己去「理解」人设,要把人设变成它只需要执行、不需要理解的明确指令。
2. State as Data
把动态心理、关系、目标和剧情状态从模型注意力黑箱中抽离,变成系统可观测、可干预的结构化数据。状态更新器是 State Reducer,基于上一轮状态增量更新,而不是每轮凭空重新分析。
3. Consistency as Harness
用状态更新、记忆检索、示例检索、生成后验证和自动重写构成完整的质量闭环,不把可靠性寄托在单次模型调用上。不要等用户看到漂移了再纠正,要在返回之前就拦住问题。
写在最后
这篇文章提出的是一个关于长对话角色一致性的工程假设与可验证架构,而不是一套已经被大规模验证的成熟技术方案。
但我相信这个方向是对的。当我们需要构建可靠的、生产级别的 AI 应用时,不能再把所有问题都推给提示词工程。模型是一个强大的生成器,但它不是一个可靠的状态机。把状态管理、一致性检查和错误恢复的责任从模型那里拿回来,交给专门设计的系统去做,这才是构建可靠 AI 应用的正确思路。
未来的 AI 应用架构,必然会越来越多地看到这种「模型能力 + 工程 harness」的模式。而我们现在做的,只是这个大趋势下一个具体的案例而已。