写在前面
事情起因
本人在使用Gemini 3 Pro进行网络小说创作以及文字冒险、角色扮演叙事时,都会在第一轮对话输入系统设定以及各种必要文档。
然而网页端最近对话从第三十轮左右开始,模型不再参考任何文档,经过对比测试分析,发现官网极大概率大幅度缩短了发给模型的上下文窗口,并非降智或者注意力机制,而是采用滑动窗口对上下文输入进行物理阉割,只取最近的20~30轮对话发给大模型(Gemini 2.5 Pro时期是100轮以上甚至有200轮)。
我不知道谷歌抽什么风,但这将对专门使用Gemini长上下文(记忆需求)、高召回率功能的用户(小说、剧本创作;长对话聊天;长文翻译;长代码创作;文献综述;论文写作;文字互动游戏;角色扮演叙事等)造成极大影响。
为何使用网页端?
Google AI Studio以及API并没有网页版的窗口滑动问题,但该平台对所有非付费API用户一视同仁,配额很少,最要命的是输出的截断问题,即便关闭安全设置,模型也会频繁截断输出,尤其是涉及恐怖灵异、刑侦推理、血腥惊悚、黑暗元素小说创作、版权、社会问题讨论,尤其是含有限制内容的创作,如破限科研、“自由式”角色扮演,在此可以明确告诉大家,网页端是完全没有任何截断风险的!
网页端配额
模型当日能够使用有两个必要条件,其一是未达到RPD(每日请求次数)上限,其二是未达到TPD(每日输入Tokens限制)上限,缺一不可,必须同时满足,前者是订阅方案内明确告知的,后者为了临时调整算力分配而隐藏,但凡有一个不满足,就要等待第二天配额刷新。
忽略滑动窗口问题,每次输入,并非只传达用户最新的一句提示,而是会将整个上下文窗口打包发送给AI,这意味着AI的每次输出都会包含在下次的输入中,不断累加,这样在一个上下文窗口内,TPD的累计增加是非常迅速的。
因此,对于长上下文对话的用户来说,最大的痛点是隐藏的TPD限制,往往RPD还远未达到上限,TPD就已经触界了。
Pro用户的配额为100RPD,以及大约3~6MTPD(单一对话持续大约60轮);
Ultra用户的配额为500RPD、足量(我曾经在一天内不断对话直到第二天刷新也没提示我达到使用限制)TPD。
对具有长记忆需求的用户来说,Ultra本来是完美方案,在谷歌还没有调整滑动窗口时,一个Ultra账户就足以提供任何支撑,而目前面对滑动窗口的机制,ultra方案也就没有了任何意义,它相当于是从20~30轮对话开始,每个对话截取最近20轮的消息,不断新开对话窗口。这完全与长记忆背道而驰,至于最开始的系统提示和必要文件,已经消失得无影无踪。模型会突然开始背离小说作者发送的大纲路线;突然开始阉割代码用户程序本来拥有的功能;突然把游戏玩家的角色属性大改;会突然告诉已经破限的角色扮演用户:“对不起,我不能继续这个对话……”
需求动机
我们迫切需要一种能延续对话记忆的手段,从而保证叙事的顺序以及正确的注意力权重,在今年年初(25年2、3月份)的时候,当时是为了解决对话过长后报错无法继续的问题,基于模型输入的原理(将所有聊天记录,通过json格式标注发言对象,一次性打包发给AI),我尝试过将聊天记录拉到最上方,接着全选整个页面,复制粘贴所有内容。但这种内容无法标注用户和模型的信息,开新对话后模型会出现明显的幻觉,以及错误的注意力。而手动标注工作量不堪设想,AI标注输出限制又不够,最终,放弃了这种方法,现在的滑动窗口问题异曲同工。
解决方法
现在我们利用脚本来导出对话并直接标注发言对象,以仿照json格式的上下文。
- 电脑(我用的windows系统,Chrome浏览器)进入Gemini官网。
- 打开你要导出聊天记录的对话窗口。
- 【重要】滑动滚轮/单击滚轮拖动鼠标/拖拽右侧滚动条,将窗口拉至整个对话的最上方,直到加载出第一条对话(就是你在这个窗口里给AI发的第一条提示,通常是系统设定),目的是为了让网页加载出所有对话信息。
- 按下F12, 点击 Console (控制台) 选项卡,并点击图中红笔标注位置
- 在该位置粘贴如下脚本代码(没错,这个脚本是用Gemini 3 Pro写的)
(function() {
console.log("🚀 启动 V10.0 互斥锁精准导出...");
// 结果存储
let items = [];
let seenNodes = new Set();
// 辅助函数:向上找最近的独立内容块
// isModel = true (找模型), false (找用户)
function findContentBlock(startNode, isModel) {
let current = startNode.parentElement;
let bestContainer = startNode.parentElement; // 默认回退
// 向上找 10 层
for (let i = 0; i < 10; i++) {
if (!current || current.tagName === 'MAIN' || current.tagName === 'BODY') break;
// === 互斥锁核心逻辑 ===
// 1. 如果我们在找【模型】,但当前框里发现了【用户的复制按钮】,说明爬过头了(进到了父级大框)
// 必须立刻停下,并返回上一个节点(current 的子节点,也就是 bestContainer)
if (isModel) {
if (current.querySelector('button[aria-label="复制提示"]')) {
return bestContainer;
}
}
// 2. 如果我们在找【用户】,但当前框里发现了【模型的听回答按钮】,也说明爬过头了
if (!isModel) {
if (current.querySelector('button[aria-label="听回答"]')) {
return bestContainer;
}
}
// 如果当前层看起来是个正经容器(有文字),暂存它为“最佳候选”
// 继续往上爬,直到触发互斥锁或爬完次数
if (current.innerText.length > 1) {
bestContainer = current;
}
current = current.parentElement;
}
return bestContainer;
}
// ============================
// 1. 抓取用户 (User)
// ============================
const userBtns = document.querySelectorAll('button[aria-label="复制提示"]');
userBtns.forEach(btn => {
let container = findContentBlock(btn, false);
if (container && !seenNodes.has(container)) {
items.push({ role: 'User', node: container, text: container.innerText });
seenNodes.add(container);
}
});
// ============================
// 2. 抓取模型 (Gemini)
// ============================
const modelBtns = document.querySelectorAll('button[aria-label="听回答"]');
modelBtns.forEach(btn => {
let container = findContentBlock(btn, true);
if (container && !seenNodes.has(container)) {
items.push({ role: 'Gemini', node: container, text: container.innerText });
seenNodes.add(container);
}
});
// ============================
// 3. 排序、清洗与导出
// ============================
items.sort((a, b) => a.node.getBoundingClientRect().top - b.node.getBoundingClientRect().top);
if (items.length === 0) {
alert("未提取到内容,请确保页面加载完成。");
return;
}
let finalContent = "Gemini Chat Export (V10.0 Fixed)\n" +
"Export Date: " + new Date().toLocaleString() + "\n" +
"========================================\n\n";
items.forEach(item => {
let text = item.text;
// --- 温和清洗 (不再使用 .* 强杀) ---
// 仅仅移除按钮文字本身,保留换行结构
const junkWords = [
"复制提示", "修改", "听回答", "显示更多选项", "显示思路",
"Show drafts", "Regenerate", "Google 隐私权政策"
];
junkWords.forEach(word => {
// 全局替换关键词为空,但不删后面的内容
text = text.split(word).join("");
});
// 移除末尾多余的空行
text = text.trim();
// 简单去重:如果这段话和上一段完全一样,跳过 (防止父子容器双重抓取漏网)
if (text.length > 0) {
finalContent += `【${item.role}】:\n${text}\n\n${"-".repeat(20)}\n\n`;
}
});
// 下载
const blob = new Blob([finalContent], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `Gemini_Chat_V10_${new Date().toISOString().slice(0,10)}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
console.log(`✅ V10 导出完成!共 ${items.length} 条。`);
})();
- 按回车“ENTER”,会弹出下载一个TXT文件,里面就是导出的对话记录。
进阶使用与微调
判断何时该使用脚本导出记录切新对话窗口
- 在移动端使用(网页端没有明确显示),第一条对话一定要上传一个文档,一般大部分任务都是有文件上传的,没有要上传文件的话,可以随便编辑一个文档,内容就是短短的一句话:“务必遵循系统设定。”这样的文档内容不会影响模型的输出。在第一条对话还在上下文窗口内的时候,模型输出的内容最下方会有一个《信息来源》的引用按钮,我用一个重生古代复仇角色扮演叙事的内容来举个例子,如下图:
- 当后端开始滑动窗口物理截断你的上下文时,第一条对话一定是没了,此时模型的输出下面就完全没有《信息来源》的按钮了:
此时你再问:“你能看到我上传的文档吗?”
它就开始说自己没有读取文档的能力,然后你就以为是降智了,其实不是昂,是输出的上下文被物理截断了,这时就该导出聊天记录了。
接着去电脑导出所有记录,然后视情况把最后一轮对话删掉(因为最后一次回复模型没有参考你的第一条输出和文档,并不是很准确。当然,事无绝对,如果你喜欢这条输出,就不用删)。
使用方法
- 拿到聊天记录,那是一个.txt文件,里面开头有个表示数据时间的无意义信息
把这个删掉,不要占上下文。 - 【注意事项】第一次切对话时,上下文大概会在5万tokens左右,相对于Gemini本身的上下文来说微不足道,因此是可以直接放进一个对话框的,但是切记不要把这个聊天记录放在新窗口第一条对话,和系统设定占一起,会影响模型的注意力。
因此,我们需要对聊天记录微调一下,将第一条的User内容删除(这段就是你发的系统设定),系统设定不是不要了,而是要单独作为第一条内容发给AI,为了避免模型立即开始回复正式内容,需要在系统设定最后加一句:
接下来我将发送本次对话的历史记录。
这时模型就会让你发历史记录:
貌似在我写这篇文章的时候,网页端更新了,现在也有《来源》了。
值得注意的是,我们上传的文档在导出的聊天记录里只显示文档的名字和格式,其中的内容是没任何办法导出的:

但我们不需要导出,因为文档本来就在我们的电脑或者谷歌云端硬盘里。我们只需要将文档和系统设定一起作为第一条信息发给AI,这样前置工作就做完了,具体形式如下:
文字部分:【系统设定】+接下来我将发送本次对话的历史记录。
文件部分:文件。
- 接下来,直接将对话历史发过去就行,务必确保最后一句话是你要发的最后一条输出:
【历史记录】
---
{User}:
(你的最新提示)
完毕。
此时,我们解决了原先对话的问题(系统设定、必要文件丢失和前述剧情的截断)以及新开对话后面临的文风问题(5万tokens的聊天记录足以让模型基本完美复刻原先的叙事风格)
进阶使用
上述情况是切一次对话,差不多能够持续到10万tokens的使用,但是对相当一部分人来说,这肯定是不够的。
10万tokens时,模型开始出现明显的召回率问题,开始“忘记”一些事情,我们如法炮制再去切对话,还是可以的,不过新的输出,出错率会明显上升。
如何保证输出质量,并大大延续能够使用的极限,下面详细叙述第二次切新对话的方式:
- 打开上一次对话(第一次)的聊天记录文件,将第一条用户消息(系统设定+文件)内容删掉,留下的是从模型第一条输出到最后一条输出的文本,大概5万tokens。
- 发给Gemini,用本身模型也行,用Deep Research也罢,让它们
用严谨、科学的态度撰写该剧情的百科资料,所有信息的出现以及顺序必须能够在原文中有所体现。
之后,会得到一段剧情总结,先存起来。 - 接下来,导出本次对话(第二次)的聊天记录,并去掉时间戳信息和系统提示以及第一次聊天记录(【重要】第一次聊天记录我们已经总结了,不需要原先的长文了,不过这会导致叙事风格问题,别着急,下面的操作会完美解决这个问题),得到第二次对话聊天记录。
- 仿照刚才《使用方法》中的步骤2。
- 【重点】对《使用方法》中的步骤3进行微调:
将步骤2中的剧情总结复制粘贴到你新一次对话(第三次)的第二条对话(第一条是系统设定和文件)框中,并将步骤3中得到的第二次对话聊天记录也粘贴上去,确保末尾是你的最新提示,输入框内容由《使用方法》中步骤3的变为:
【第一次对话聊天记录总结】
---
【第二次对话历史记录】
---
{User}:
(你的最新提示)
可以发现,步骤3中的问题也不再是问题,虽然第一次对话历史没了,但我们新增了第二次对话历史记录以维持文风。
第三次、第四次切对话只需要重复:
- 总结上次对话(注意,已经总结过的对话不需要总结,记得去掉它)
- 导出本次对话
- 仿照重复进阶使用中剩余步骤
最终得到:
【第一次对话聊天记录总结】
---
【第二次对话聊天记录总结】
---
……
---
【第(N-1)次对话聊天记录总结】
---
【第N次对话历史记录】
---
{User}:
(你的最新提示)
这一方法用聊天总结缩短了上下文,用新的历史记录替代了旧的历史记录,维持了叙事风格,因此一般情况下总可以保持对话质量,并且大幅度提高有效叙事长度。
写在最后
随着切对话次数增多,第二条对话会越来越长,直到达到10万tokens以上,这时对话质量还是会下降,我们可以将前几次总结的历史(称为“原始历史总结”)再次进行进一步精简和总结,然后用新总结的历史替换掉原始历史总结,然后将原始历史总结作为文件放到新一次对话的第一条输入中,从而保证必要时网页端模型可以进行RAG检索,提高准确度。
那么,希望能帮上各位L友的忙。




