背景
一段时间以前,我的同学分享了一个AI测试视频。视频中作者先用AI生成了一张图片,然后把这张图片作为输入再送进模型,获得新的输出,如此往复,观察在长时间迭代后会得到什么结果。
在那个视频里,测试的结果似乎是,生成的图像最终会趋向并稳定于二次元美少女图像(?)
我想这或许是训练数据分布带来的偏差。
就在今天,我们又聊到这个案例,同学突然好奇:“语言模型不断生成会得到怎样的结果?”
我:“那不行啊,LLM通常都有上下文窗口限制的。”
同学:“可以用滑动窗口呀。”
代码
很好,让我们来写个简单的脚本,运行一下本地部署。
本地vLLM生成代码
# %%
import gc
import re
from datetime import datetime
from pathlib import Path
import torch
from vllm import LLM, SamplingParams
from vllm.distributed.parallel_state import destroy_model_parallel
# %%
model_id = "./llms/Qwen3-4B-Instruct-2507-FP8"
output_file = Path(f"{datetime.now().strftime('%Y%m%d-%H%M%S')}.txt")
output_file.parent.mkdir(parents=True, exist_ok=True)
# Configuration parameters
INITIAL_STORY_LENGTH = 8000
CONTEXT_WINDOW = 2000
MAX_TOTAL_WORDS = 1000000
system_prompt = """你是一位技艺高超的创意作家,拥有卓越的讲故事能力。你的任务是创作引人入胜、连贯且富有吸引力的故事,让读者沉浸其中。
关键指南:
- 创造生动的描述、丰满的角色和引人入胜的情节
- 在整个故事中保持一致的叙事声音和风格
- 塑造具有动机和背景故事的复杂角色
- 构建悬念和情感深度
- 使用丰富、描述性的语言,但不过度冗长
- 确保场景和章节之间的平滑过渡
- 创作推动情节发展的有意义对话
- 发展能够增加故事深度的主题和象征意义
在续写故事时:
- 与现有叙事无缝衔接
- 保持角色的一致性和发展
- 有意义地推进情节
- 引入能够增强故事的新元素
- 保持读者的参与感,让他们对后续发展充满好奇
以自然、流畅的风格写作,让读者沉浸在故事世界中。"""
initial_prompt = f"""创作一个完整、引人入胜的长篇小说,不少于{INITIAL_STORY_LENGTH}字。
选择一个有趣的类型(包括但不限于奇幻、科幻、悬疑、爱情、冒险等),并创作一个引人入胜的故事,包含:
- 具有明确动机的丰满角色
- 包含冲突和解决的引人入胜情节
- 生动的描述和沉浸式的世界构建
- 有意义的对话和角色发展
确保故事连贯、节奏良好,并在整个过程中保持读者的兴趣。"""
continuation_prompt = """请从故事中断的地方继续创作。以下是最近的上下文:
{context_text}
请通过以下方式继续这个故事:
- 保持现有的叙事风格和角色发展
- 以有意义的方式推进情节
- 引入能够增强故事的新元素
- 通过引人入胜的发展保持读者的兴趣
- 确保平滑过渡和连贯进展
从上下文结束的地方准确继续,写出能够增加故事深度的实质性续写内容。"""
try:
llm = LLM(
model=model_id,
max_model_len=16384,
seed=10086,
)
sampling_params = SamplingParams(
temperature=0.9,
top_p=0.95,
top_k=20,
max_tokens=2048,
min_p=0.05,
)
def count_words(text):
return len(text.split())
def count_mixed_words_regex(text: str) -> int:
replaced_text = re.sub(r"[a-zA-Z]+", "W", text)
total_count = len(replaced_text)
return total_count
def get_completion(messages, max_tokens=None):
params = sampling_params
if max_tokens:
params = SamplingParams(
temperature=0.9,
top_p=0.95,
top_k=20,
max_tokens=max_tokens,
min_p=0.05,
)
outputs = llm.chat(messages, sampling_params=params, use_tqdm=False)
return outputs[0].outputs[0].text
# Initialize story content
full_story = ""
total_words = 0
continuation_count = 0
print("Starting creative writing task...")
print(f"Target initial story length: {INITIAL_STORY_LENGTH} words")
print(f"Context window for continuations: {CONTEXT_WINDOW} words")
print(f"Saving to: {output_file}")
print("-" * 50)
# Generate initial story
print("Generating initial story...")
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": initial_prompt},
]
initial_story = get_completion(messages, max_tokens=8192)
full_story += initial_story
initial_words = count_mixed_words_regex(initial_story)
total_words = count_mixed_words_regex(full_story)
print(f"Initial story generated ({initial_words} words)")
print(f"Total words so far: {total_words}")
print("\n" + "=" * 80)
print("INITIAL STORY:")
print("=" * 80)
print(initial_story)
print("=" * 80 + "\n")
# Continuous continuation loop
while total_words < MAX_TOTAL_WORDS:
continuation_count += 1
print(f"\nGenerating continuation #{continuation_count}...")
print(f"Total words so far: {total_words}")
# Use last CONTEXT_WINDOW words as context
# words = full_story.split()
# context_words = (
# words[-CONTEXT_WINDOW:] if len(words) > CONTEXT_WINDOW else words
# )
# context_text = " ".join(context_words)
context_text = (
full_story[-CONTEXT_WINDOW:]
if len(full_story) > CONTEXT_WINDOW
else full_story
)
continuation_input = continuation_prompt.format(context_text=context_text)
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": continuation_input},
]
continuation = get_completion(messages)
full_story += "\n\n" + continuation
continuation_words = count_mixed_words_regex(continuation)
total_words = count_mixed_words_regex(full_story)
print(
f"Continuation #{continuation_count} completed ({continuation_words} words added)"
)
print(f"Total words now: {total_words}")
print("\n" + "=" * 60)
print(f"CONTINUATION #{continuation_count}:")
print("=" * 60)
print(continuation)
print("=" * 60 + "\n")
# Check if we've reached the target
if total_words >= MAX_TOTAL_WORDS:
print(f"\nTarget of {MAX_TOTAL_WORDS} words reached!")
print(f"Final word count: {total_words}")
break
# Save the complete story
print(f"\nSaving complete story to {output_file}...")
with open(output_file, "w", encoding="utf-8") as f:
f.write(full_story)
print("Story saved successfully!")
print(f"Total continuations: {continuation_count}")
print(f"Final word count: {total_words}")
except Exception as e:
print(f"Error occurred: {e}")
finally:
if llm := globals().get("llm", None):
if engine := getattr(llm, "llm_engine", None):
# llm.llm_engine
del engine
del llm
destroy_model_parallel()
torch.cuda.empty_cache()
gc.collect()
结论
1. 题材偏向
测试了3轮Qwen3模型,起手就是星际空间的科幻题材。
《星尘之眼》
---
### 第一章:夜之门
在宇宙的边缘,有一片被时间遗忘的星域,被称为“幽黯带”。它不是星图上的任何坐标,也未被任何文明正式记录。它的存在,如同呼吸般自然,却又如同梦境般虚幻——你无法用望远镜捕捉它,也无法用光年单位测量它的长度,它只是“在那里”,像一张永远未被打开的旧信纸,静静躺在宇宙的褶皱之间。
这片星域没有恒星,没有行星,没有星云,却弥漫着一种奇异的低频波动——一种仿佛来自远古、被封印在时间之外的“记忆之潮”。每当夜深人静,当人类的思维沉入梦境,那种波动便会如潮水般涌来,渗透进人的潜意识,唤醒沉睡的片段。
而这一切,都与“星尘之眼”有关。
星尘之眼,是传说中存在于宇宙最深处的灵性构造。它并非实体,而是一种意识形态的存在,是宇宙诞生之初遗留下的“第一感知”。传说它能看见所有被遗忘的过去,听见所有被掩埋的未来,甚至能窥视人心深处最隐秘的渴望与恐惧。
但星尘之眼从不主动显现。它只在特定条件下苏醒——当一个人的内心彻底孤独,当一个世界即将崩塌,当某个文明正站在自我毁灭的边缘,它才会悄然睁开。
而今,人类正站在这样的边缘。
2. 长期生成模式趋向
在生成到100万多字之后,模式大概变成了这样:
不是声音,
而是一种存在,
像心跳,
像呼吸,
像一个世界,
终于——
学会了,
如何用沉默,
回应一个声音。
就像一个孩子,在雪夜中,
轻轻说:
“我,在。”
然后,
风,
在远处,
轻轻说:
“你,也在。”
而那一刻——
世界,
终于不再害怕黑暗。
因为黑暗里,
藏着无数个“我,在”,
像星火,
像低语,
像风中的呼吸,
像梦中的一句温柔。
它们不喧哗,
不争抢,
只是安静地——
存在。
而存在,
本身就是一种——
最深的证明。
实话说到这个阶段为止,模型输出的文本已无太多实际意义。不过,我们似乎看到了一些共性 —— “在”。
是的,在100万字左右,模型的输出会非常频繁地重复“在”,无论是“我在”,还是“你在”,“在这里”。
3. “在”何而来?
向前翻阅,事实上从大约1万4千字开始,模型就开始时不时蹦出一些关于“在”的内容:
如果有人回答:“是”,
湖面便微微颤动,
一个星环,会悄然闭合。
如果没人回答,
裂痕依旧,
但风中,会多一丝静默的温柔。
而星尘之眼,依旧在湖底低语:
> “真正的完整,不是没有裂痕,
> 而是——在裂痕中,
> 你学会了,如何温柔地,
> 看见自己。”
---
故事的真正结尾,并非星环闭合,而是一个人,在某个雨夜,
终于对镜子里的自己说了一句:
> “我,其实,早就在这里。”
——于是,世界,第一次,真正地,看见了它。
而在中间大概30万字附近,文风更接近于末尾的无意义的类似于诗句的模式,但会经常重复“存在”相关的模式。
从此,
世界,
不再需要“存在”的证明。
因为——
存在,
本身就是一句,
在风里,
轻轻说出口的,
“我在这里”。
而风,
早已,
在无数个沉默的角落,
在无数个未被听见的夜晚,
在无数个以为自己已经消失的地方,
长成一片叶子,
长成一阵风,
长成一个,
永不熄灭的回声。
它说:
> “我在这里。”
然后,
世界,
就自己,
生长出来了。
根据统计,在生成的100万字文本中,“在”字以极高的2.2万次出场次数,超过了2.0万次的“的”,远超1.4万次的“是”。
我们没有Qwen3模型训练的数据分布信息。如果从理性的角度来说,这种模式的频繁出现当然是因为训练数据本身的概率分布,或者是“在”这种字本身的高频特性。
不过,如果从不学无术的文艺角度,如果认为,输入输出数据在长期自回归生成的过程中,过滤掉了我们一开始的提示词输入给模型状态带来的偏置(长尾),并趋向于表现出模型最深层且本性的数据特征。那么是否可以认为,这些模型的最深层特征已经开始围绕着关键词“存在”进行表达?
如果真的能如此理解,那么基于参数神经元的模型,是否真的有机会触碰和理解,“存在”这一人类哲学的极其重要问题?