作者 | 崔皓
審校 | 重樓
摘要
本文作者受到一位國外博主的啟發(fā),決定嘗試使用大語言模型創(chuàng)建一個地下城文字游戲。通過大語言模型生成富有創(chuàng)意和連貫性的游戲內(nèi)容。他的游戲靈感主要來源于經(jīng)典的桌面角色扮演游戲“龍與地下城”(D&D)。該游戲通過對話驅(qū)動,包括兩個主要角色:故事描述者和主人公。故事描述者負責下達任務和推動故事,而主人公負責完成任務。作者設計了一個對話模擬器,用于處理角色之間的互動和游戲進程。此外,作者還考慮了游戲設計中的兩個關鍵問題:記憶機制和發(fā)言規(guī)范。
開篇
自從在短視頻平臺上看到一位國外博主通過AI創(chuàng)建了一個模擬經(jīng)營游戲,我就被深深吸引了。在他的虛擬小鎮(zhèn)上,眾多的NPC人物因AI的賦能而擁有了各種獨特的性格,使得他們之間的互動充滿了趣味和驚喜。這讓我有點心癢癢,一直對游戲情有獨鐘的我,為什么不嘗試借助AI的力量,創(chuàng)造一個屬于自己的地下城文字游戲呢?然而,缺乏游戲開發(fā)經(jīng)驗成了我面前的一道障礙。不過轉(zhuǎn)念一想,最近正在從事大語言模型開發(fā)的工作,而大語言模型本身就具備生成文字的能力。 我看到有人利用提示詞和大模型可以玩文字的冒險游戲。帶著這個想法,我查找了一些資料,發(fā)現(xiàn)通過prompt的方式真的可以“催眠”大模型,讓它幫我創(chuàng)建一個游戲世界。
探索與嘗試
有了上面的想法就要付諸行動了,說干就干,不過在開干之前需要對游戲進行構思,同時還要對實施的可行性進行評估。我的探索開始于ChatGPT,通過手動輸入的方式,我和ChatGPT玩起了文字游戲。很快,我發(fā)現(xiàn)它與流行的龍與地下城游戲有異曲同工之妙—一個基于角色扮演的奇幻世界,玩家通過解決棘手的問題、勇敢的探險和激烈的戰(zhàn)斗來推進故事。
龍與地下城(Dungeons & Dragons, 簡稱D&D)是一款源于1974年的桌上角色扮演游戲,它允許玩家在一個奇幻的中世紀環(huán)境中探險和戰(zhàn)斗。玩家可以選擇不同的角色,如戰(zhàn)士、法師或盜賊,并通過擲骰子來決定行動的結(jié)果。游戲由一名地下城主(Dungeon Master, DM)來引導故事和管理游戲規(guī)則。D&D不僅創(chuàng)造了桌上角色扮演游戲的標準,也影響了后來的電子游戲和奇幻文學的發(fā)展。它的核心是集體講述故事、解決問題和角色扮演,為玩家提供了一個豐富多彩、想象力驅(qū)動的游戲體驗。
我可以通過龍與地下城游戲的玩法藍本,來設計我的文字游戲。由于我沒有任何的游戲設計經(jīng)驗,因此將游戲規(guī)則設計得簡單明了,主要通過角色間的對話來推進游戲進程,以降低新手玩家的入門門檻。這里對話角色分為兩個,分別是故事描述者和主人公,通過對話交互,玩家能夠逐步深入游戲的核心,挑戰(zhàn)和完成各種任務,同時享受與虛擬角色交流的樂趣。
趁熱打鐵,我們來設計一下文字游戲具體完成的任務。如下圖所示,在文字游戲初始化的時候需要定義參與者,包括:主角(主人公)和故事的講述者,故事的講述者負責下達任務,而主人公負責完成任務。當然他們之間通過文字完成任務的交互,這里需要定義一個發(fā)言的規(guī)則。故事講述者下達任務之后,主角會接著發(fā)言,推進故事,接著通過輪流發(fā)言的方式完成所有的任務。
準備出發(fā)
有了思路以后就要準備開發(fā)所需要的模型和工具了。模型方面,我選擇了OpenAI 的GPT-3.5-Turbo的版本,另外也考慮了使用百度千帆模型庫中提供的ChatGLM2-6B-32K模型,不過在輸入字符長度上前者比較有優(yōu)勢,因此還是選擇了GPT,不過后面代碼的部分,我會把ChatGLM2版本的代碼以remark的方式呈現(xiàn)給大家,如果大家有興趣可以自行嘗試。
有了游戲背景,接著就需要考慮文字游戲都會遇到的兩個問題:
記憶機制
在地下城游戲中,保持對話的連貫性和角色身份的一致性至關重要。為解決這個問題,我們設計了一個消息系統(tǒng),它可以保存參與者的背景信息,包括:身份,發(fā)言的方式,以及哪些事情應該做,哪些事情不能做。每次提供提示給大模型時,都會利用該系統(tǒng)強化模型的記憶,確保游戲的流暢進行。剛好可以利用LangChain中的System Message 機制完成。
LangChain的`SystemMessage`是一個特殊的消息類型,它用于在與大語言模型的交互中提供上下文或指令。通過`SystemMessage`,可以向模型傳達關于游戲的背景信息或其他重要指示,幫助模型生成更符合場景的回應。它是在構建交互式應用時,提供上下文和指導信息的有效方式。
發(fā)言規(guī)范
為保證游戲的有序進行,我們需要設定明確的發(fā)言順序。在這個簡單的場景中,我們選擇了輪流發(fā)言的規(guī)則。為了未來能夠輕松擴展到更多玩家或更復雜的交互規(guī)則,我們將發(fā)言規(guī)則獨立為一個模塊,從而為游戲提供了更多的靈活性。在實施的時候,也就是通過一個取模的函數(shù)就能夠輕松搞定。如果遇到多人參與,或者存在交叉發(fā)言,玩家互相提問的場景,也可以設計更加負責的發(fā)言規(guī)則??傊枰獙⒁?guī)則的部分獨立出來,做到高內(nèi)聚低耦合。
程序設計
有了游戲設計思路,接著我們對基本的程序進行設計。如下圖所示,在入口的Main函數(shù)中我們會進行對話參數(shù)的初始化,例如:人物基本信息,背景信息的定義。然后,調(diào)用DialogueSimulator 添加玩家,同時設置發(fā)言規(guī)則,以及發(fā)起任務。接下來就是通過DialogueAgent 類進行文字消息的處理,這里只需要處理發(fā)送消息和接受消息。由于,本例中只有兩個參與者,故事描述者和主人公所以發(fā)送和介紹消息會在兩者之間輪流進行。
代碼實踐
完成了基本的設計工作,就是寫代碼了。這里我們使用Colab作為IDE工具,以及如何在此環(huán)境中配置和使用Langchain和GPT-3.5 Turbo。
Colab,即Google Colaboratory,是一個免費的Jupyter筆記本環(huán)境,無需進行任何設置就可以在瀏覽器中使用。它不僅提供了可靠的計算資源,還配備了GPU和TPU,可以加速機器學習或深度學習項目。Colab的特點包括實時多人協(xié)作、免費GPU資源、易于分享和集成Google Drive等。使用Colab的主要原因是它的便捷性和高效性。它省去了配置環(huán)境的麻煩。
首先,通過如下代碼在colab中安裝包和相關依賴:
!pip install openai langchain
該命令用于安裝Python包,以支持后續(xù)的開發(fā)工作:
1. `openai`:OpenAI的Python客戶端庫。
2. `langchain`:可能是與LangChain相關的庫,支持與大語言模型的交互。
DialogueAgent 類
一個簡單的封裝器,圍繞 ChatOpenAI 模型進行操作,通過簡單地將消息作為字符串連接起來,從而存儲 dialogue_agent 角度看到的消息歷史。
它提供了兩個方法:
- send():將 chatmodel 應用于消息歷史,并返回消息字符串。
- receive(name, message):將由 name 發(fā)出的消息添加到消息歷史中。
# 定義DialogueAgent類,用于表示對話中的一個代理(或參與者)
class DialogueAgent:
# 初始化方法,用于設置代理的基礎屬性
def __init__(
self,
name: str, # 代理名稱:參與者,故事描述者,主人公
system_message: SystemMessage, # 系統(tǒng)消息對象
model: ChatOpenAI, # 聊天模型
) -> None:
self.name = name # 設置代理名稱
self.system_message = system_message # 設置系統(tǒng)消息
self.model = model # 設置聊天模型
self.prefix = f"{self.name}: " # 設置消息前綴,用于標識消息來源
self.reset() # 調(diào)用reset方法初始化消息歷史
# reset方法,用于重置或初始化消息歷史
def reset(self):
self.message_history = ["Here is the conversation so far."] # 初始化消息歷史
# send方法,用于生成并發(fā)送消息
def send(self) -> str:
"""
Applies the chat model to the message history
and returns the message string
"""
# 調(diào)用模型生成消息
message = self.model(
[
self.system_message, # 系統(tǒng)消息:背景
HumanMessage(content="\n".join(self.message_history + [self.prefix])), # 人類消息(包含歷史和前綴)
]
)
#message = self.model(
# "\n".join([
# str(self.system_message),
# str(HumanMessage(content="\n".join(self.message_history + [self.prefix])))
# ])
#)
#return message.content
return message # 返回生成的消息
# receive方法,用于接收并記錄消息
def receive(self, name: str, message: str) -> None:
"""
Concatenates message spoken by {name} into message history
"""
# 將接收的消息加入到消息歷史中
self.message_history.append(f"{name}: {message}")
DialogueSimulator 類
DialogueSimulator 類接受一個代理列表。在每一步中,它執(zhí)行以下操作:
選擇下一個發(fā)言者。調(diào)用下一個發(fā)言者來發(fā)送消息。將消息廣播給所有其他代理。更新步驟計數(shù)器。下一個發(fā)言者的選擇可以通過任何函數(shù)來實現(xiàn),但在這種情況下,我們簡單地遍歷代理。
代碼如下:
# 定義一個DialogueSimulator類來模擬對話
class DialogueSimulator:
# 初始化方法,用于設置類的基本屬性
def __init__(
self,
agents: List[DialogueAgent], # 代理列表,包含所有對話的參與者
selection_function: Callable[[int, List[DialogueAgent]], int], # 選擇下一個說話者的函數(shù)
) -> None:
self.agents = agents # 將傳入的代理列表保存為實例變量
self._step = 0 # 初始化步數(shù)為0
self.select_next_speaker = selection_function # 設置選擇下一個說話者的函數(shù)
# 重置方法,用于重置所有代理的狀態(tài)
def reset(self):
for agent in self.agents: # 遍歷每個代理
agent.reset() # 調(diào)用每個代理的reset方法進行重置
# 初始化對話的方法
def inject(self, name: str, message: str):
"""
Initiates the conversation with a message from a specific agent
"""
for agent in self.agents: # 遍歷每個代理
agent.receive(name, message) # 讓每個代理接收初始消息
self._step += 1 # 增加步數(shù)計數(shù)
# 進行一步對話的方法
def step(self) -> tuple[str, str]:
# 1. 選擇下一個說話的代理
speaker_idx = self.select_next_speaker(self._step, self.agents) # 使用選擇函數(shù)選擇代理
speaker = self.agents[speaker_idx] # 獲取選中的代理
# 2. 讓選中的代理發(fā)送消息
message = speaker.send() # 獲取代理發(fā)送的消息
# 3. 讓所有代理接收這條消息
for receiver in self.agents: # 遍歷所有代理
receiver.receive(speaker.name, message) # 讓每個代理接收消息
# 4. 增加步數(shù)計數(shù)
self._step += 1 # 更新步數(shù)
# 返回說話代理的名字和消息內(nèi)容
return speaker.name, message
定義角色
在這段代碼中,定義了四個變量,用于存儲故事或任務的關鍵信息。
這四個變量為我們在進一步編寫或生成故事提供了基礎信息。
protagonist_name = "馬小虎"
storyteller_name = "神秘老人"
quest = "找到傳說中的七件神器。"
word_limit = 50
1. `protagonist_name = "馬小虎"`:這一行定義了主角(故事中的主要人物)的名字為"馬小虎"。
2. `storyteller_name = "神秘老人"`:這一行定義了講故事的人(故事的敘述者)的名字為"神秘老人"。
3. `quest = "找到傳說中的七件神器。"`:這一行定義了主角需要完成的任務或探險目標。
4. `word_limit = 50`:這一行定義了用于任務的字數(shù)限制為50字。在進行任務時,需要設置一個字數(shù)限制以保持焦點和簡潔性。
故事背景描述
下面這段代碼主要用于生成一個地下城冒險游戲的描述和提示。它涉及到兩個主要角色:主人公(由變量 `protagonist_name` 定義)和故事講述者(由變量 `storyteller_name` 定義)。代碼的目的是通過自然語言模型(在這里是 GPT-3.5 Turbo 或一個名為 "QianfanLLMEndpoint" 的模型)來生成這兩個角色的詳細描述。
game_description = f"""這是一場地下城冒險游戲: {quest}.
游戲中存在一名玩家: 主人公, {protagonist_name}.
故事由故事的講述者來描述, {storyteller_name}."""
player_descriptor_system_message = SystemMessage(
content="你可以為這場地下城冒險游戲添加細節(jié)."
)
#主人公提示詞
protagonist_specifier_prompt = [
player_descriptor_system_message,
HumanMessage(
content=f"""{game_description}
請用創(chuàng)意的方式來描述主人公, {protagonist_name}, 描述字數(shù)不超過 {word_limit} 個.
并且說出主人公的名字, {protagonist_name}.
除此之外不要添加其他信息."""
),
]
#llm 生成對主人公的描述
protagonist_description = ChatOpenAI(model_name="gpt-3.5-turbo",temperature=1.0)(
protagonist_specifier_prompt
).content
#llm = QianfanLLMEndpoint( model="ChatGLM2-6B-32K", temperature = 1.0)
#protagonist_description= llm.generate([str(protagonist_specifier_prompt)])
#故事講述者提示詞
storyteller_specifier_prompt = [
player_descriptor_system_message,
HumanMessage(
content=f"""{game_description}
請用創(chuàng)意的方式來描述故事講述者, {storyteller_name}, 描述字數(shù)不超過 {word_limit} 個.
并且說出故事講述者的名字, {storyteller_name}.
除此之外不要添加其他信息."""
),
]
#llm 生成對故事描述者的描述
storyteller_description = ChatOpenAI(model_name="gpt-3.5-turbo",temperature=1.0)(
storyteller_specifier_prompt
).content
#storyteller_description = llm.generate([str(storyteller_specifier_prompt)])
1. 生成游戲描述: 使用前面定義的 `quest`, `protagonist_name`, 和 `storyteller_name` 變量來形成一個完整的游戲描述 (`game_description`)。
2. 創(chuàng)建系統(tǒng)消息:一個名為 `SystemMessage` 的類被用來生成一個提示消息,提示玩家可以為這場游戲添加更多細節(jié)。
3. 生成主人公描述:使用自然語言模型和一個特定的提示 (`protagonist_specifier_prompt`) 來生成主人公的描述 (`protagonist_description`)。
4. 生成故事講述者描述:同樣地,使用自然語言模型和一個特定的提示 (`storyteller_specifier_prompt`) 來生成故事講述者的描述 (`storyteller_description`)。
我們將上面的信息打印出來驗證一下:
print("Protagonist Description:")
print(protagonist_description)
print("Storyteller Description:")
print(storyteller_description)
打印內(nèi)容:
Protagonist Description:
馬小虎,一個年輕而勇敢的劍士,有著黑色的蓬松頭發(fā)和劍刃一樣銳利的眼神。
Storyteller Description:
故事講述者: 神秘老人,耄耋仙人。
主角與地下城主的系統(tǒng)消息
這段代碼生成一個更具體和詳細的任務描述(`specified_quest`),該任務描述是為地下城冒險游戲的主角(`protagonist_name`)準備的。
# 定義一個名為 quest_specifier_prompt 的列表,包含 SystemMessage 和 HumanMessage 對象
quest_specifier_prompt = [
# 系統(tǒng)消息,提示可以使任務更具體
SystemMessage(content="你可以使任務更具體。"),
# 人類消息,要求故事講述者更具體地描述任務
HumanMessage(
content=f"""{game_description}
你是故事講述者,{storyteller_name}。
請使任務更具體化。請富有創(chuàng)意和想象力。
請用{word_limit}個詞或更少回復指定的任務。
直接對主角{protagonist_name}說話。
不要添加其他任何內(nèi)容。"""
),
]
specified_quest = ChatOpenAI(model_name='gpt-3.5-turbo',temperature=1.0)(quest_specifier_prompt).content
#specified_quest = llm.generate([str(quest_specifier_prompt)])
print(f"原始任務描述:\n{quest}\n")
print(f"詳細任務描述:\n{specified_quest}\n")
1. 定義任務指定提示(`quest_specifier_prompt`)**:這個列表包含兩種類型的消息對象:`SystemMessage` 和 `HumanMessage`。`SystemMessage` 提供了一種簡單的提示,即"你可以使任務更具體"。`HumanMessage` 則給出了詳細的指示,包括當前的游戲描述、故事講述者的名字,以及對任務應如何具體化的要求。
2. 生成具體任務(`specified_quest`)**:使用前面定義的 `quest_specifier_prompt` 和自然語言模型(在這里是 GPT-3.5 Turbo)來生成一個更具體和詳細的任務描述。
3.*輸出結(jié)果:最后,代碼打印出原始的任務描述和新生成的更具體的任務描述。
打印結(jié)果如下:
output
原始任務描述:
找到傳說中的七件神器。
詳細任務描述:
馬小虎,你需要穿越一條幻想之河,進入寒冰之洞穴。在那里,你將遇到一只被詛咒的巨龍。喚醒它的心靈,獲取神器第一件:冰霜之劍。完成任務,勇士。
哈哈,此時我們的參與者(故事描述者和主角)都已經(jīng)有了,并且各自的背景都準備好了,任務也隨之生成了。接下來就是執(zhí)行游戲的部分了。
主函數(shù)入口
這段代碼的主要目的是模擬一個交互式故事,其中包括兩個角色:主人公(`protagonist`)和故事講述者(`storyteller`)。這些角色在一個預定義的對話模擬器(`DialogueSimulator`)中輪流互動。
#主人公
protagonist = DialogueAgent(
name=protagonist_name,
system_message=protagonist_system_message,
model=ChatOpenAI(model_name='gpt-3.5-turbo',temperature=0.2),
#model = llm,
)
#故事描述者
storyteller = DialogueAgent(
name=storyteller_name,
system_message=storyteller_system_message,
model=ChatOpenAI(model_name='gpt-3.5-turbo',temperature=0.2),
#model = llm,
)
#發(fā)言順序:輪流發(fā)言
def select_next_speaker(step: int, agents: List[DialogueAgent]) -> int:
idx = step % len(agents)
return idx
max_iters = 12
n = 0
simulator = DialogueSimulator(
agents=[storyteller, protagonist], selection_function=select_next_speaker
)
simulator.reset()
#故事描述者 , 任務的描述
simulator.inject(storyteller_name, specified_quest)
print(f"({storyteller_name}): {specified_quest}")
print("\n")
while n < max_iters:
name, message = simulator.step()
print(f"({name}): {message}")
print("\n")
n += 1
1. 初始化角色:使用 `DialogueAgent` 類創(chuàng)建兩個不同的角色,其中包括他們的名字、系統(tǒng)消息和使用的模型(這里是 GPT-3.5 Turbo)。
2.選擇發(fā)言人函數(shù): `select_next_speaker` 函數(shù)用于確定在對話中哪個角色應該接下來發(fā)言。這里使用了一個簡單的輪流機制。
3.對話模擬:使用 `DialogueSimulator` 類創(chuàng)建一個對話模擬器,該模擬器接受角色列表和選擇發(fā)言人的函數(shù)。
4.注入初始任務:使用 `simulator.inject()` 方法將特定的任務描述(`specified_quest`)作為故事講述者的初始發(fā)言注入。
5.對話循環(huán):在一個循環(huán)中,角色輪流發(fā)言,直到達到最大迭代次數(shù)(`max_iters`)。
執(zhí)行代碼我們可以看到如下的結(jié)果,如下圖所示看來馬小虎和神秘老人的文字游戲進展還是比較順利的,隨著他們的對話,給我們揭開了一個奇幻的游戲世界。
總結(jié)
本文展示了如何用AI技術創(chuàng)建一個簡單的文字游戲,是對大語言模型應用的一次嘗試。通過使用大語言模型,作者能夠生成復雜性的游戲內(nèi)容,從而提供了富有想象力的游戲體驗。文章還強調(diào)了模塊化在游戲設計中的重要性,特別是在處理記憶和發(fā)言規(guī)范等方面。是一個非常有趣的項目,不僅展示了大語言模型在游戲開發(fā)中的應用,也為那些對AI和游戲感興趣的人提供了啟示和經(jīng)驗。
作者介紹
崔皓,51CTO社區(qū)編輯,資深架構師,擁有18年的軟件開發(fā)和架構經(jīng)驗,10年分布式架構經(jīng)驗。