先说判断方向:SocialCoach 这类对话助手“聊三轮就变套话”,通常不是一个原因,而是三个变量叠在一起——每轮实际发出去的消息被裁剪或截断、系统提示只写了情绪要求没写行为要求、以及模型本身在上下文变长后的稳定性差异。能不能确定是哪一种,取决于你手上有没有每轮发送内容的日志。没有日志时,人很容易把“提示词太笼统”误判成“记忆丢了”。可操作的顺序是:先量化现象,再打印长度,再改提示词,最后用单变量对照复跑来分离三个原因。
前两轮具体、第三轮开始全是安慰话,建议先去会话记录里标记转折轮次,再打印每轮发送的消息条数与字符数,确认是消息被裁剪还是提示词太宽泛。多数情况下,把系统提示里的“要温柔、要共情”改写成可检验的行为要求(复述事实、追问缺失信息、给出下一步动作),输出会明显变具体。若上下文长度和提示词都排除后现象仍在,再考虑模型或参数本身的差异。所有结论都应以你自己环境里的日志和行为为准。
在会话记录里定位从第几轮开始变空泛
不要凭聊天界面的感觉说“它记不住”,先给会话记录补上字段。建议至少记录这些列,写入你们自己的日志表或本地文件即可:
- round:轮次,从 1 开始递增,每轮用户一次输入算一轮;
- user_input:用户原始输入,不改写、不截断;
- model_output:模型原始输出,保留完整文本;
- sent_msg_count:这一轮实际发送给模型的消息条数(含系统提示那一项,如何计入口径要统一);
- system_chars:系统提示字符数;
- note:人工标注“具体 / 半具体 / 空泛”,标注标准先定好,例如“是否出现了用户提过的具体人名、场景或时间点”。
比对方法是逐轮读:从第 1 轮开始,找出第一轮 model_output 不再引用用户前面说过的具体信息的那一轮,把它标成转折点。然后看转折点前后 sent_msg_count 有没有异常——如果 count 突然不再增长、甚至回落,先怀疑上下文被裁剪;如果 count 正常增长、system_chars 也很小,先怀疑提示词。这里的判断是方向性的,需要结合你的实际记录确认,不要用一轮的观感下结论。
打印每轮实际发送出去的消息条数与字符数
这一步的目的是把“上下文满了”从猜测变成可看的数字。在调用模型的前后各打一次日志,放在你现有请求封装里即可,与具体 SDK 无关:
def log_round(round_no, messages, system_prompt):
total_chars = sum(len(m.get("content", "")) for m in messages)
print({
"round": round_no,
"sent_msg_count": len(messages), # 实际发出去的消息条数
"total_chars": total_chars, # 所有消息字符数合计
"system_chars": len(system_prompt), # 系统提示长度
"roles": [m.get("role") for m in messages], # 看最早的几轮是否被丢掉
})
def call_model(round_no, messages, system_prompt, model_cfg):
log_round(round_no, messages, system_prompt)
resp = client.chat(messages=messages, system=system_prompt, **model_cfg)
print({"round": round_no, "resp_chars": len(resp.text or ""),
"finish_reason": getattr(resp, "finish_reason", None)})
return resp
怎么判断是否触到上限:把 total_chars 和你配置里的上下文上限、max_tokens 放在一起看。如果 total_chars 长期贴近上限、且 roles 列表里最早的用户消息消失,基本可以判定触发了裁剪逻辑。如果 total_chars 离上限还有明显余量、消息条数正常增长、finish_reason 也没有截断标记,那“上下文满了”这个解释就站不住,应该转向提示词和模型。注意:不同接入方式对“一条消息”的计数口径不同,系统提示有时单列、有时并入 messages,前后统计要统一,否则数字会误导你。
把系统提示里的共情类指令改成可检验的行为要求
“温柔一些、多共情、像朋友一样”这类描述模型无法验收,只能给出通用的安慰话。改法是把每一轮输出拆成可检查的动作。修改前:
你是一个温柔、有同理心的社交教练。请和用户共情,语气自然友好,像朋友一样聊天,给出你的建议。
修改后(按需调整字段名,重点是动作可验收):
你是社交教练。每轮回复必须包含以下三部分,缺一不可:
1. 复述:用一句话复述用户本轮说过的事实,只允许出现用户提过的人物、场景、原话,不得补充推断。
2. 追问:提出一个具体问题,用来补齐缺失信息(对方原话是什么 / 用户希望达成什么 / 有没有时间限制)。
3. 下一步:给出一个用户明天可以执行的动作,写清楚动作、场合、以及可以直接照说的一句话示例。
约束:
- 禁止以“我理解你”“这很正常”“你已经很棒了”这类通用安慰作为主要回复内容;
- 若用户信息不足,只做追问,不给建议;
- 不得复述与当前问题无关的旧话题。
验证方式:用同一段输入连跑,检查第 3 轮输出里是否同时出现“用户提过的具体事实”“一个问题”“一个带示例句的动作”。三项里缺哪一项,就说明对应的提示词条款没写清楚,而不是模型记不住。
固定同一段输入重复跑三次,只改一个变量
分离变量靠一张最小对照表,每跑一次只允许动一格,其余全部锁死。
- 第一轮:现有上下文策略 + 旧提示词 + 原模型;
- 第二轮:只改提示词(换成本文中改写后的版本),上下文策略和模型不动;
- 第三轮:只改上下文策略(例如固定只保留最近若干轮,或不做任何裁剪),提示词和模型不动。
记录表模板:
| 次数 | 上下文策略 | 提示词版本 | 模型/温度 | 第3轮输出是否含具体事实 | 是否含追问 | 是否含下一步动作 | 备注 |
|------|------------|------------|-----------|--------------------------|------------|------------------|------|
| 1 | 现状 | 旧版 | 原模型 | | | | |
| 2 | 现状 | 新版 | 原模型 | | | | |
| 3 | 固定最近N轮| 新版 | 原模型 | | | | |
“只改一个变量”是这张表的全部意义。如果第二轮输出变具体,问题主要在提示词;如果第二轮仍然空泛、而第三轮改变上下文后变具体,问题在上下文处理;如果三轮表现都差不多,才需要进一步怀疑模型和参数。注意温度、max_tokens 这类参数在三次里要保持一致,否则记录表就失去了对照价值。
根据现象给出三分支结论和下一步动作
结论一:上下文被截断。 判据是 total_chars 长期贴近上限、或早期用户消息从发送列表里消失。处理动作:改为固定保留最近若干轮原文,把更早的对话压成一段事实摘要;把关键事实(对方姓名、约定时间、用户目标)单独抽成一条常驻的系统侧信息,不随轮次被裁掉。验证方式:同样的输入重跑,看第 3 轮起是否重新引用具体事实。风险边界:摘要有信息损耗,涉及承诺、金额、时间点的事实建议原样保留,不要只留摘要。
结论二:提示词太空。 判据是消息条数正常增长、total_chars 离上限有富余,但输出始终停在通用安慰。处理动作:按上一节的写法,把每轮输出改成“复述 + 追问 + 下一步”的可验收结构,并显式禁止通用安慰作为主体内容。验证方式:同一输入连跑三次,观察结构是否稳定出现。风险边界:约束过死会让回复变得机械,可以根据场景只保留其中一到两项必填。
结论三:模型或参数本身的差异。 只有在上下文和提示词两个变量都被排掉后才成立,判据是三次对照表现接近、日志里看不出裁剪、提示词已结构化。处理动作:换一档模型对比、适当降低温度、或改成结构化输出后自行渲染文案;同时缩短单次上下文长度。验证方式:同样的输入和提示词换模型复跑,看第 3 轮是否仍然空泛。风险边界:不要把“换模型”当成万能修复,长对话里的信息保持能力需要按你的真实场景反复试,别用一次观察就下终局判断。