NRIネットコム社員が様々な視点で、日々の気づきやナレッジを発信するメディアです

注目のタグ

    Lambda + DynamoDB + LangChainで外部情報に対応したサーバレスチャットボットを作ってみる

    Lambda + DynamoDB + LangChainを組み合わせて外部情報に対応したチャットボットを作ってみました。

    構成

    Lambda

    LangChainのパッケージが250MBを超えてしまうので、コンテナイメージからLambda関数を作成するようにします。

    docs.aws.amazon.com

    Dockerfile

    FROM public.ecr.aws/lambda/python:3.10
    
    # Copy requirements.txt
    COPY requirements.txt ${LAMBDA_TASK_ROOT}
    
    # Copy function code
    COPY lambda_function.py ${LAMBDA_TASK_ROOT}
    
    RUN pip install --upgrade pip
    
    # Install the specified packages
    RUN pip install -r requirements.txt
    
    # Set the CMD to your handler (could also be done as a parameter override outside of the Dockerfile)
    CMD [ "lambda_function.handler" ]  

    requirements.txt

    langchain==0.0.262
    google-search-results==2.4.2
    openai==0.27.8

    lambda_function.py

    from langchain import OpenAI,SerpAPIWrapper
    from langchain.agents import initialize_agent, Tool
    from langchain.agents import AgentType
    from langchain.chat_models import ChatOpenAI
    from langchain.prompts import MessagesPlaceholder
    from langchain.memory import ConversationBufferMemory
    from langchain.memory.chat_message_histories import DynamoDBChatMessageHistory
    
    llm = ChatOpenAI(temperature=0, model="gpt-3.5-turbo-0613")
    search = SerpAPIWrapper()
        
    tools = [
        Tool(
            name="Search",
            func=search.run,
            description="useful for when you need to answer questions about current events. You should ask targeted questions",
        )
    ]
    
    agent_kwargs = {
        "extra_prompt_messages": [MessagesPlaceholder(variable_name="chat_history")],
    }
    
    def handler(event, context):
        
        session_id = event["session_id"]    
        prompt = event["prompt"]
    
        message_history = DynamoDBChatMessageHistory(table_name="SessionTable", session_id=session_id)
            
        memory = ConversationBufferMemory(
            memory_key="chat_history", chat_memory=message_history, return_messages=True
        )
        
        agent = initialize_agent(
            tools,
            llm,
            agent=AgentType.OPENAI_FUNCTIONS,
            verbose=True,
            agent_kwargs=agent_kwargs,
            memory=memory,
        )
        
        output = agent.run(prompt)
        
        return {
            'statusCode': 200,
            'body': output
        }
    

    OpenAI Functions Agent

    今回はAgentにOpenAI Functions Agentを使用しています。これはOpenAIのFunction callingに対応したAgentで、個人的に従来のAgentに比べてより動作が安定している印象があるのでこちらを使用します。

    https://python.langchain.com/docs/modules/agents/agent_types/openai_functions_agentpython.langchain.com

    llmにはFunction callingに対応したgpt-3.5-turbo-0613以降のモデルを使用する必要があることに注意してください。

    llm = ChatOpenAI(temperature=0, model="gpt-3.5-turbo-0613")
    
    agent = initialize_agent(
            tools,
            llm,
            agent=AgentType.OPENAI_FUNCTIONS,
            verbose=True,
            agent_kwargs=agent_kwargs,
            memory=memory,
        )
    

    会話履歴の保持

    今回はAgentを使用してますが、何も設定しないと過去のメッセージ履歴を保持してくれません。そこで今回はDynamoDBChatMessageHistoryを使って履歴を保持するようにしています。 またsession_idを指定することで、他の人の会話履歴が混ざらないようsession_idごとに履歴を分けて保存するようになっています。

    https://python.langchain.com/docs/integrations/memory/dynamodb_chat_message_historypython.langchain.com

    message_history = DynamoDBChatMessageHistory(table_name="SessionTable", session_id=session_id)
        
    agent_kwargs = {
        "extra_prompt_messages": [MessagesPlaceholder(variable_name="chat_history")],
    }
        
    memory = ConversationBufferMemory(
        memory_key="chat_history", chat_memory=message_history, return_messages=True
    )
         
    

    外部情報の参照

    今回は外部情報の参照ができるように、AgentのToolにserpAPIでGoogle検索を行うSerpAPIWrapper()を使用します。serpAPIのAPI_KEYを取得し、Lambdaの環境変数にSERPAPI_API_KEYとして定義しておきます。

    search = SerpAPIWrapper()
        
    tools = [
        Tool(
            name="Search",
            func=search.run,
            description="useful for when you need to answer questions about current events. You should ask targeted questions",
        )
    ]
    

    DynamoDB

    チャット履歴の保持で用いるDynamoDBを作成します。

    aws dynamodb create-table \
        --table-name SessionTable \
        --attribute-definitions AttributeName=sessionid,AttributeType=S \
        --key-schema AttributeName=sessionid,KeyType=HASH \
        --billing-mode PAY_PER_REQUEST

    Lambda関数の実行ロールにDynamoDBに対する書き込み、参照権限を追加する必要があるので追加しておきます。

    動作確認

    Lambda関数を実行してチャットしてみます。 今回はGoogle検索を行わないと回答できない最新の情報について質問してみます。

    実行ログ

    [1m> Entering new AgentExecutor chain...[0m
    [32;1m[1;3m
    Invoking: `Search` with `2023年 現在の日本の首相`
    [0m[36;1m[1;3mFumio Kishida is a Japanese politician serving as prime minister of Japan and president of the Liberal Democratic Party since 2021. A member of the House of Representatives, he previously served as Minister for Foreign Affairs from 2012 to 2017 and as acting Minister of Defense in 2017.[0m[32;1m[1;3m2023年現在、日本の首相は岸田文雄(きしだ ふみお)氏です。彼は2021年以降、日本の首相および自由民主党の党首を務めています。彼は衆議院の議員であり、2012年から2017年まで外務大臣、2017年には臨時の防衛大臣を務めました。[0m
    [1m> Finished chain.[0m 

    実行ログから「2023年 現在の日本の首相」で検索をおこない、正しい回答を導き出していることがわかります。

    チャット履歴を保持しているか確認してみましょう。

    ちゃんと過去の会話履歴を覚えていることがわかります。

    今度はsession_idを変えて同じ質問をしてみます。

    今回は前回の質問内容をおぼえていません。これによりAgentがDynamoDBから会話履歴をsession_idごとに取得していることが確認できました。

    まとめ

    今回はLambda + DynamoDB + LangChainを組み合わせて外部情報に対応したチャットボットをサーバレスで作ってみました。今回はLambda関数を実行しているだけですが、SlackやLINEなどと連携するようにしても面白そうです。LangChainを使うことで簡単にチャットボットが実装できるのでぜひお試しください。

    執筆者堤 拓哉

    インフラエンジニア。データ分析やデータ基盤周りの話に興味があります。