Slide 1

Slide 1 text

ChatGPTを使ったSlackbotの実装いろいろ紹介 LLM Meetup Tokyo #2 mkazutaka

Slide 2

Slide 2 text

自己紹介 名前: mkazutaka 所属: 株式会社エクスプラザ もともとキャンプ系のアプリとウェブ作ってた 最近はLLM案件をやるように 業務委託を覗いて一人目エンジニアなので色々やってます

Slide 3

Slide 3 text

Slackbot作ってる 立ち位置的には、LLM TeamのPlayground なのでWaitingListで提供する人絞ってる

Slide 4

Slide 4 text

Plutoで実装したもの ストリーミング機能の実装 発言者がだれかを認識させる実装 チャンネルごとにプロンプトの設定できる機能

Slide 5

Slide 5 text

以下はすべてLangChainを使った話です

Slide 6

Slide 6 text

ストリーミング機能の実装: 概要 生成されたテキストを一度に表示するより、生成途中のテキストも含めて表示されたほ うが体験が良い LangChainのChatModelには、Streaming機能がある SlackはStreaming機能をサポートしてないので、疑似ストリーミングを実装する i. メッセージを投稿 ii. メッセージを都度編集する

Slide 7

Slide 7 text

ストリーミング機能の実装: 実装 LangChainには、Slack連携をサポートしているクラスはない LangChainの AsyncCallbackHandler を継承したクラスを作る on_llm_start , on_llm_new_token , on_llm_end のイベント時に起こるフック できる

Slide 8

Slide 8 text

ストリーミング機能の実装: コード 一部抜粋 class StreamingAsyncSlackCallbackHandler(AsyncCallbackHandler): ... async def on_llm_start( self, serialized: Dict[str, Any], prompts: List[str], **kwargs: Any ) -> None: # Slack にメッセージを送信 self.initial_message = loop = asyncio.get_event_loop() self.task = loop.create_task(self._update_chat()) async def _update_chat(self): while True: await asyncio.sleep(0.5) # Slack のメッセージを更新 ...

Slide 9

Slide 9 text

発言者がだれかを認識させる実装

Slide 10

Slide 10 text

発言者がだれかを認識させる実装: 概要 Slackは、多対多のコミュニケーションツールなので、誰がどの発言したかが必要 やることはシンプルで、プロンプトを以下形式にに設定する system: hi mkazutaka: hi

Slide 11

Slide 11 text

発言者がだれかを認識させる実装: 実装 LangChainで、Modelにチャット形式でテキストを渡すには、一般に MessagePromptTemplate を使う。 ただし、ユーザ名に system や human といったものしか指定できない なので、 MessagePromptTemplate を使わず、 PromptTemplate をつかった 今ドキュメント見てると ChatMessagePromptTemplate があり、ここでroleを設定でき そうであった

Slide 12

Slide 12 text

チャンネルごとにプロンプトの設定できる機能

Slide 13

Slide 13 text

チャンネルごとにプロンプトの設定できる機能: 概要 Slackには、チャンネルがあり、それぞれ専門の会話ができる Plutoもそれにあわせ、チャンネルごとに特定のプロンプトや返答方法(スレッドを 使うか否か)を設定できる 設定の変更は、チャット上から行える

Slide 14

Slide 14 text

チャンネルごとにプロンプトの設定できる機能: 実装 LangChainのAgentを使う LLMが指定したツールの中から自動的に選んで実行してくれる Agentには、自作ツールを2つ渡す プロンプト設定のツール 返答方法設定のツール SlackメッセージからChatGPTにメッセージをリクエストする際に、設定したプロンプ トを付与する

Slide 15

Slide 15 text

チャンネルごとにプロンプトの設定できる機能: コード 1/2 Toolの例 class ChannelConfigurationThreadModeTool(BaseTool): name = "ChannelConfigurationThreadModeTool" description = f"""A wrapper Channel Configuration Tool that can be used to set up thread mode. Input should be "True" or "False" string with one keys: "thread_mode". The value of "thread_mode" should be a "True" if the user wants to use thread, otherwise "False". Call only if Thread mode is specified. """ def _run( self, thread_mode: str, run_manager: Optional[CallbackManagerForToolRun] = None ) -> str: # DB に保存 return "Successfully set up thread mode"

Slide 16

Slide 16 text

チャンネルごとにプロンプトの設定できる機能: コード 2/2 Agent実行する部分 llm = ChatOpenAI( model_name="gpt-3.5-turbo", temperature=0, ) executor = initialize_agent( [ self.channel_configuration_thread_mode_tool, self.channel_configuration_prompt_tool, ], llm, agent=AgentType.CHAT_ZERO_SHOT_REACT_DESCRIPTION, ) result = await executor.arun(message)

Slide 17

Slide 17 text

チャンネルごとにプロンプトの設定できる機能 無事実行できる

Slide 18

Slide 18 text

はい、ダウト うまく行かないケースが3つほどある

Slide 19

Slide 19 text

うまくいかないケース1: OutputParserに失敗する 実行後のOutputをParseする部分でExceptionがでる 成功後のエラーなので、最悪無視してもよいけど気持ち悪い langchain.schema.OutputParserException: Could not parse LLM output: The prompt has been set up successfully.

Slide 20

Slide 20 text

うまくいかないケース1: 原因 LLMからの出力は以下のどちらかが前提で書かれている Final Answer: が出力に含まれている テキストが```の文字列ででスプリットできる # 実際のコード抜粋 FINAL_ANSWER_ACTION = "Final Answer:" if FINAL_ANSWER_ACTION in text: return AgentFinish(...) try: action = text.split("```")[1] response = json.loads(action.strip()) return AgentAction(response["action"], response["action_input"], text) except Exception: raise OutputParserException(f"Could not parse LLM output: {text}") 今回はレスポンスはその両方を満たしていない

Slide 21

Slide 21 text

うまくいかないケース1: なぜこんな実装に? Agentのプロンプトを見るとわかる Agentのプロンプトがレスポンスの形式を指定している 次のActionがあれば```で囲まれたActionのInput 最後ならFinal Answerの文字列を返すよう指定 # 一部抜粋 ALWAYS use the following format: Question: the input question you must answer Thought: you should always think about what to do Action: ''' $JSON_BLOB ''' Observation: the result of the action ... (this Thought/Action/Observation can repeat N times) Thought: I now know the final answer Final Answer: the final answer to the original input question"""

Slide 22

Slide 22 text

うまくいかないケース1: 解決策 わからん プロンプトをいじったけど変わらず 結局: FINAL_ANSWER判定のロジックをゆるくした 完全に解決してない if FINAL_ANSWER_ACTION in text or 'success' in text.lower(): return AgentFinish( {"output": text.split(FINAL_ANSWER_ACTION)[-1].strip()}, text )

Slide 23

Slide 23 text

うまくいかないケース2: Actionのパースに失敗する LLMから出力されるActionが以下のようにオブジェクトが2つで返ってくる json.loads(action.strip()) を使ってパースを試みるので失敗する Action: ''' { "action": "ChannelConfigurationThreadModeTool", "action_input": { "thread_mode": "True" } } { "action": "ChannelConfigurationPromptTool", "action_input": { "prompt": " テスト" } } '''

Slide 24

Slide 24 text

うまくいかないケース2: 解決方法 わからん(2度目) プロンプトをいじったけど変わらず 結局: json_loadの判定をmultiに対応できるようにした while actions: response, idx = decoder.raw_decode(actions.strip()) responses.append(response) next_idx = actions.find('{', idx) if next_idx == -1: break actions = actions[next_idx:] agent_actions = [ AgentAction(response["action"], response["action_input"], text) for response in responses ] return agent_actions[0] if len(agent_actions) == 1 else agent_actions

Slide 25

Slide 25 text

うまくいかないケース2: 注意点 OutputParserの抽象クラスは下記のようになっている class AgentOutputParser(BaseOutputParser): @abstractmethod def parse(self, text: str) -> Union[AgentAction, AgentFinish]: """Parse text into agent action/finish.""" 前ページの変更をすると、出力の型が抽象クラスと異なることに注意する 変更後: Union[Union[AgentAction, List[AgentAction], AgentFinish]] ただしOutputParserを呼び出す部分は、実は List[AgentAction] に対応している ので動く 参考: https://github.com/hwchase17/langchain/pull/2362

Slide 26

Slide 26 text

うまくいかないケース3: 実行してほしいActionは一つな のに複数実行する スレッドのみを変更した旨のメッセージを送るが、Agentはスレッドの変更とプロンプ トの変更を行う

Slide 27

Slide 27 text

うまくいかないケース3: 解決方法 わからん(3度目) 定期的にうまくいくのでうーんっていう気持ち

Slide 28

Slide 28 text

LLM Meetupに参加して という発表をLLM Meetupのデモでやりました。 色んな人と話した結果、GPT-4を使って解決してくれるのを待つか、Agent特化のFine Turingを作るのが良さそうという話になった。ありがとうございました

Slide 29

Slide 29 text

以上