彻底搞懂LangGraph【之三】:构建一个创作电影脚本的多智能体应用


点击上方蓝字关注我们

LangGraph是著名的大模型开发框架LangChain推出的用于构建基于复杂工作流的LLM应用的开发库。LangGraph把任务的节点与关系用Graph结构来定义以支持更多样更复杂的应用场景,特别是:

  • 实现包含循环、迭代等复杂工作流的高级RAG范式

  • 需要更灵活控制的Agent应用,如指定Tool、增加人机交互等

  • 多智能体系统(Multi-Agent System)的构建


我们在之前的文章中曾经介绍过LangGraph实现C-RAG,本文将分享一个如何使用LangGraph构建一个创作电影场景与脚本的多智能体应用

如果你需要回顾LangGraph,请参考:

彻底搞懂LangGraph:构建强大的Multi-Agent多智能体应用的LangChain新利器

彻底搞懂LangGraph【之二】:构建一个可自我纠正的增强知识库RAG应用


关于多智能体系统


AI智能体是一个基于大模型的具备自我感知、规划与行动能力的AI应用。而多智能体系统(MAS)顾名思义就是由多个AI智能体构成,通过相互关联与协作共同完成任务的智能体系统。

MAS可以是每个智能体有自己独立的LLM、提示词、Tools或者其他自定义代码,用来与其他智能体协作,比如一个虚拟的AI软件公司,可以由多个独立的LLM智能体来担任架构师、程序员、测试人员等多个角色;也可以是一个LLM在不同的提示下扮演不同的角色,比如在一个电影虚拟世界中,同一个LLM根据提示扮演不同的虚拟人物。

电影创作的多智能体应用


在这个应用中,我们的目标是实现一个能够自动创建电影场景、并能够代入场景中的多个角色,模拟对话生成台词脚本,最后输出剧本的LangGraph应用。在这个应用中,需要AI完成的任务是:

  • 场景创建:让AI根据简单输入创建一个电影场景与若干角色。

  • 角色模拟与创作:AI模拟场景中的不同角色进行脚本创作,推动情节发展。


设计如下的Graph图,来表示这个应用的工作流:

大致的流程描述如下:

  • create_scene:用AI创作一个简单电影场景与若干角色。

  • select_speaker:AI根据情节发展选择与切换人物角色,除非故事结束。

  • handle_dialogue:AI模拟选择的人物角色,根据情节发展进行多轮对话。

  • write:完成后将会把整个电影场景与对话脚本输出成文件。


这里的多智能体体现在AI会根据提示扮演不同的角色,并根据情节发展做自主对话。其中角色的切换和扮演由select_speaker与handle_dialogue循环进行,直到满足结束条件(故事结束或者到达最大对话次数)。

用LangGraph实现应用


现在根据上面的Graph来实现这个应用。先做一个简单的准备工作,实现一个LLM调用方法,这里我们使用本地的Ollama加载qwen2中文模型:

from typing import Dict, TypedDict, Optional
from langgraph.graph import StateGraph, END
from langchain.output_parsers import CommaSeparatedListOutputParser
from langchain_community.llms import Ollama
from langchain_openai import ChatOpenAI
from docx import Document

# 模型调用
llm_qwen = Ollama(model='qwen2')
llm_openai = ChatOpenAI(model='gemini-pro')
def llm(x):
    response = llm_qwen.invoke(x)
    return response

【定义提示词】

我们需要定义如下大模型使用的提示词,分别用来:

  • 创建场景:根据输入的题材创建一个简单场景及相关角色

  • 提取角色:也就是AI将要扮演的电影中的多个角色名称

  • 角色选择:根据情节发展选择下一个登场的角色

  • 角色模拟:让AI切换扮演电影中的不同角色进行对话并推动情节发展

prompt_scene = "设计一个需要台词的电影场景,题材是:{}。简单描述故事的背景和角色名称,但不要设计台词。不超过100字。角色不超过4个。"
prompt_roles = "识别执行此电影场景所需的不同角色,仅以逗号分隔的列表形式输出最简单的角色名称,角色名称不要包含头衔或称呼、称号、昵称等。这是电影场景:{}"
prompt_select_speaker = """
请根据电影场景、已有对话内容,从以下角色中选出适合下一个说话的人。
如果没有对话内容,请选择一个角色开始对话。如果故事结束,输出END。
-----------
{}
-----------
请仅输出以上的角色名称,名称必须完全匹配。
电影场景:
-----------
{}
-----------
当前对话内容:
-----------
{}
-----------
"""

prompt_speak = """
你现在是{},根据下面对话和场景,说出你的下一段台词。输出格式:
------
{}:台词
------
要求:
1. 台词与场景和角色的设定相符。
2. 台词能推动剧情发展。
3. 不要重复之前的台词。
到目前为止的对话内容:
----------
{}
----------
电影场景:
----------
{}
----------
"""

【定义GraphState】

这是LangGraph工作流运行中的共享状态数据,也就是各个流程节点之间交换的数据,通常把流程中需要共享的数据在此统一定义。这里我们做如下定义:

# state定义
class GraphState(TypedDict):
    #下一个说话者
    next_speaker: Optional[str] = None

    #对话历史
    history: Optional[str] = None

    #当前对话内容
    current_response: Optional[str] = None

    #当前说话者
    current_speaker: Optional[str] = None

    #剩余对话轮数,为0则结束
    dialogues_left: Optional[int]=None

    #电影场景
    scene: Optional[str]=None

    #电影题材
    subject: Optional[str]=None

    #电影角色
    roles: Optional[str]=None

    #最后输出
    results: Optional[str]=None

【创建场景与角色:create_scene】

根据设定主题生成一个电影场景与故事背景,并从场景中抽取出多个角色用于后续的AI模拟。角色存放到roles,场景存放到scene:

#创建场景与角色
def create_scene(state):
    scene = llm(prompt_scene.format(state.get('subject')))
    actors = llm(prompt_roles.format(scene))

    output_parser = CommaSeparatedListOutputParser()
    roles = output_parser.parse(actors)

    print(f"Scene created: {scene}")
    print(f"Actors created: {roles}\n")

    return {"scene":scene,"roles":roles}

【角色选择】

根据场景、对话历史、角色列表选择下一个合适对话的角色;如果输出为END,表示故事与对话结束。

#选择说话者
def select_speaker(state):
    speaker
= state.get('current_speaker')
    scene = state.get('scene')
    summary = state.get('history', '').strip()
    roles = state.get('roles')
    
    next_speaker = llm(prompt_select_speaker.format(','.join(roles),scene,summary))
    
    if next_speaker == "END":
        return {"dialogues_left":0}
    
    return {"next_speaker": next_speaker}

【角色模拟对话】

LLM根据提示词扮演不同的角色进行对话,对话需要基于场景与历史对话进行推理创作,完成后更改相关的state中信息:

#对话处理
def handle_dialogue(state):
    summary = state.get('history', '').strip()
    count = state.get('dialogues_left')
    next_speaker = state.get('next_speaker', '').strip()
    roles = state.get('roles')
    scene = state.get('scene')
    
    index = roles.index([x for x in roles if x in next_speaker][0])
    prompt = prompt_speak.format(roles[index],roles[index],summary,scene)
    argument = llm(prompt)
    
    print(f"{argument}\n")
    return {"history":summary+'\n'+argument,
            "current_speaker":roles[index],
            "current_response":argument,
            "dialogues_left":count-1}

【生成剧本文件】

这是最后的任务节点,把之前的创作生成一个word文件保存:

#写入剧本
def write(state):

    # Save conversation to a Word document
    doc = Document()
        
    # Add scene
    scene = state['scene']
    doc.add_heading('Scene', level=1)
    doc.add_paragraph(scene)
        
    # Add roles
    roles = state['roles']
    doc.add_heading('Roles', level=1)
    doc.add_paragraph(', '.join(roles))
        
    # Add dialogue history
    history = state['history']
    doc.add_heading('Dialogue History', level=1)
    doc.add_paragraph(history)
        
    # Save the document
    doc.save('movie.docx')

    return {"results":"剧本已生成"}

【定义Workflow】

所有的节点都已经准备完毕,现在我们可以定义应用的workflow。定义一个LangGraph的工作流的典型步骤为:

  • 增加节点:将上面创建的场景与角色创建、角色选择、角色模拟、生成剧本等通过add_node加入

  • 增加边(节点关系):按照之前设计的工作流进行连接,唯一需要注意的是流程结束条件的判断,需要使用条件函数来定义“条件边”,使得流程在场景结束或者对话次数达到上限后终止

  • 编译工作流:生成应用


#条件函数
def check_end(state):
    return "end" if state.get("dialogues_left")==0 else "continue"

workflow = StateGraph(GraphState)

#增加节点
workflow.set_entry_point("create_scene")
workflow.add_node("create_scene", create_scene)
workflow.add_node("select_speaker", select_speaker)
workflow.add_node("handle_dialogue", handle_dialogue)
workflow.add_node("write",write)

#增加边
workflow.add_edge('create_scene', "select_speaker")
workflow.add_conditional_edges(
    "select_speaker",
    check_end,
    {
        "continue": "handle_dialogue",
        "end": "write"
    }
)
workflow.add_conditional_edges(
    "handle_dialogue",
    check_end,
    {
        "continue": "select_speaker",
        "end": "write"
    }
)
workflow.add_edge('write', END)

#编译
app = workflow.compile()

【测试应用】

现在我们可以简单测试这个自动生成电影场景与脚本的应用,看看效果如何,运行如下代码:

#运行
conversation = app.invoke({'dialogues_left':20,
                           'next_speaker':'',
                           'history':'',
                           'current_response':'',
                           'subject':'一个关于三国的搞笑短视频',},
                           {'recursion_limit':50})

运行完成后,可以打开当前目录生成的movie.docx文件,查看创作的场景与台词脚本:


小结


尽管在实际运行中我们发现不同的大模型效果差异较大,需要对提示词做更针对性与精细化的调优;而且这里的多智能体也仅仅是通过提示词的简单角色模拟,未涉及复杂推理与工具的使用。但应用整体上运行良好,希望对你有所启发!后续我们将会用一个更加复杂的工具型多智能体来做进一步深入研究,敬请关注。


END



点击下方关注我,不迷路



交流请识别以下名片并说明来源


请使用浏览器的分享功能分享到微信等