推論し、行動するChatGPT(OpenAI API) Agentを作る – langchain ReAct Custom Agent基礎 –

本記事はGMOインターネットグループで複数ある開発者ブログのなかから人気の記事をピックアップして再掲載しています。機械学習や深層学習のビジネス応用を研究、社会実装をしているグループ研究開発本部 AI研究開発室のMLエンジニアが寄稿した人気記事です。

はじめに

ここ数ヶ月の生成AIの進歩の凄まじさは皆感じていることとは思います。その中で私はReActというPromptingが気に入っており、これを実装しているlangchainを使って色々試しているところであります。

そこで今回はこの推論、行動しながら外部ツールと連携する手法であるReActについて簡単に紹介し、その上でこれを実装しているlangchainを使ったCustom Agentの簡単な実装方法についてご紹介したいと思います

ReActとは?

ReAct(Yao et al., 2022)とは、Prompting Engineeringの手法の一つで、LLMに推論と行動を提示させ、その行動を元に検索などの外部ツールから追加情報を収集することによって回答を生成する手法のことです。

….と言ってもあまりイメージがわかないので実際にどんな流れになるか見てみましょう。今回はこの理論を実装したlangchainのWIKI_EXAMPLEから一例お借りして、この流れを説明したいと思います。
※以下実際の返り値とは違うのでご容赦ください。あくまでイメージです

まず最初に質問をLLM(ChatGPT/OpenAI API)に投げます。今回は「コロラド山脈東部の標高はどのくらいですか?」といったところでしょうか。ただQuestionを投げるだけでなく、LLMに[推論]させたいため、[Thought: ]という接頭辞をつけて、これを導いています

# LLM(ChatGPT)に以下の質問を投げる
Question: What is the elevation range for the area that the eastern sector of the Colorado orogeny extends into?
Thought: 

これに対して、LLM側は以下のように返します

Thought: I need to search Colorado orogeny, find the area that the eastern sector of the Colorado orogeny extends into, then find the elevation range of the area.
Action: Search[Colorado orogeny]

ここでActionというのを返していますね。ここでLLM側は質問を受けて考察し、Colorado orogenyという言葉を検索(Search)したほうが良いと提案してきたわけです。そこでここからはプログラム処理して外部ツールを連携して、この検索を実行してあげましょう。今回はwikipediaからSearchするので、こんなイメージになります

import wikipedia

result = Wikipedia.search("Colorado orogeny")

# The Colorado orogeny was an episode of mountain building (an orogeny) in Colorado and surrounding areas.
return result.summary()

ここで取得したSummaryを使って、また問い合わせをします。今度はQuestionだけでなく取得した検索結果もObservationとして付与してみましょう

Question: What is the elevation range for the area that the eastern sector of the Colorado orogeny extends into?
Observation: The Colorado orogeny was an episode of mountain building (an orogeny) in Colorado and surrounding areas.
Thought:

これを繰り返すと最終的に以下のようにLLM側がFinishと返してきます。ここでLLMは答えにたどり着いたと判断したわけですね

Action: Finish[1,800 to 7,000 ft]

このようにLLM側にThoughtという形で推論させるとともに、外部ツールとの連携を組み合わせることによってより高度な結果を得られるというわけです。より高度なPrompting Techniqueだということがなんとなくわかっていただけましたでしょうか??

またこれを応用すると現在ChatGPTで提供されているWeb Braowser機能や各種pluginも、よりカスタムできる形で実現できるのでエンジニアの方はぜひ一度当該理論の実装を覗いてみていただけるといいかと思います

langchain ReAct Custom Agent作成方法

さて基本的な動作方法はわかったので、ここでそれの簡単な実装方法をご紹介したいと思います。ただゼロから実装となると結構面倒なので、ここではlangchainと呼ばれるLLMを扱うためのライブラリを使った形のものをご紹介したいと思います。なお今回紹介するコードはv0.0.17であり、今後破壊的な変更が入る可能性が高いため、最新の実装については改めてソースコードを読んでいただけるのを推奨いたします

さて今回は既存で用意されているAgnetの接頭辞を日本語に変えるようCustomをしてみることにします。つまり以下のように接頭辞を変更する感じですね

  • Question -> 質問
  • Thought -> 考察
  • Action -> 行動
  • Finish -> 完了

まずはFew shot learning用のデータを用意します。上記でお見せした[質問->考察->行動->完了]の流れ自体は自然にChatGPT(LLM)側ができるわけでなく、この通りに動くように学ばせる必要があります。そのためまず、その学習のためのデータを用意しましょう。ここではこちらのコードを改修して、以下のようなEXAMPLESを作成します

EXAMPLES = [
    """質問: What is the elevation range for the area that the eastern sector of the Colorado orogeny extends into?
考察: I need to search Colorado orogeny, find the area that the eastern sector of the Colorado orogeny extends into, then find the elevation range of the area.
行動: Search[Colorado orogeny]
結果: The Colorado orogeny was an episode of mountain building (an orogeny) in Colorado and surrounding areas.
考察: It does not mention the eastern sector. So I need to look up eastern sector.
行動: Lookup[eastern sector]
...

そして問い合わせ文のPrompt Formatを用意します。ここのinputやagnet_scratchpadはプログラム内から動的に変更されるリプレース文字だと認識ください

SUFFIX = """\n質問: {input}
{agent_scratchpad}"""

そしてこの2つを重ね合わしたPrompTemplateを用意します。ここからもわかるように毎回EXAMPLESがくっついてくるので、Token Sizeは多くなり費用が高くかかるので本格的にやるにはFine Tuningしたほうが良さげです。ちなみに私の場合、一回問い合わせる度にdavinchiで0.2$くらいかかっております。。。

from langchain.prompts.prompt import PromptTemplate

# 基本は上記で作成したReActの構文をFewShotLearningしている
#   最終的に下記の形でJoinしている
#   template = example_separator.join([prefix, *examples, suffix])
WIKI_CUSTOM_PROMPT = PromptTemplate.from_examples(
    EXAMPLES, SUFFIX, ["input", "agent_scratchpad"]
)

さてこれで学習データは用意できたので、次は振る舞いを定義するAgentを作成します。この振る舞いをゼロから実装するのは大変なのですがlangchaindではAgentを継承すれば最低限の実装だけで済みます。以下がほぼ最低限のabstract methodを実装したものですが、先程用意したFew Shot Learningデータと、そこで使われている接頭辞を定義すれば良い形になっています。参考にしたコードはこちらになります

class ReActCustomAgent(Agent):
    """Agent for the ReAct chain."""

    output_parser: AgentOutputParser = Field(default_factory=ReActOutputParser)

    # Action句を解析して、SearchなどのActionに移るか、終わらせるかを判断する
    @classmethod
    def _get_default_output_parser(cls, **kwargs: Any) -> AgentOutputParser:
        return ReActOutputParser()

    # FewShotLearning + 質問Promptで使うための文字列を指定
    # 今回は上で作成したものを利用
    @classmethod
    def create_prompt(cls, tools: Sequence[BaseTool]) -> BasePromptTemplate:
        """Return default prompt."""
        return WIKI_CUSTOM_PROMPT

    # Observation[観察]部の接頭辞を指定
    @property
    def observation_prefix(self) -> str:
        """Prefix to append the observation with."""
        return "観察: "

    # (完全には追いきれていないが)推論を止める単語
    @property
    def _stop(self) -> List[str]:
        return ["\n観察:"]

    # thought
    #   thoughts += (f"\n{self.observation_prefix}{observation}\n{self.llm_prefix}")
    @property
    def llm_prefix(self) -> str:
        """Prefix to append the LLM call with."""
        return "考察:"

またActionとして返された文字列を解析し、どのようなプログラムを、どういった引数でKickするかを抜き出すためのParserも作成します。本来ここはあまり触らないのですが、今回はActionという文字列を変えているのでこちらも修正する必要があります.参考コードはこちらになります

import re
from typing import Union

from langchain.agents.agent import AgentOutputParser
from langchain.schema import AgentAction, AgentFinish, OutputParserException

# Prompt Exampleに合わせてテキスト処理
# Action句を解析して、SearchなどのActionに移るか、終わらせるかを判断する
class ReActOutputParser(AgentOutputParser):
    def parse(self, text: str) -> Union[AgentAction, AgentFinish]:
        action_prefix = "行動: "
        if not text.strip().split("\n")[-1].startswith(action_prefix):
            raise OutputParserException(f"Could not parse LLM Output: {text}")
        action_block = text.strip().split("\n")[-1]

        action_str = action_block[len(action_prefix) :]
        
        re_matches = re.search(r"(.*?)\[(.*?)\]", action_str)
        if re_matches is None:
            raise OutputParserException(
                f"Could not parse action directive: {action_str}"
            )
        action, action_input = re_matches.group(1), re_matches.group(2)

        # 最後が行動: Finishであれば処理を終わらせる
        if action == "完了":
            return AgentFinish({"output": action_input}, text)
        else:
            return AgentAction(action, action_input, text)

さてあとはこれらを組み合わせて動かしてみましょう.注意点としてはFew Shot Learningで用意した学習データ内のActionで定義した文字列と、実際の処理をもれなく合致させる必要があります。一応これが漏れた場合に止めるためにAgent側にvalidate関数もあるので、必要あればそちらを実装するといいかもです

## 動作確認ではada必須で!!!(
llm = OpenAI(temperature=0)

docstore = DocstoreExplorer(Wikipedia())
docstore_explorer = DocstoreExplorer(docstore)

# TemplateのAction内で指定した文字列と、実装した関数をつなぎ合わせる定義
tools = [
    Tool(
        name="Search",
        func=docstore_explorer.search,
        description="Search for a term in the docstore.",
    ),
    Tool(
        name="Lookup",
        func=docstore_explorer.lookup,
        description="Lookup a term in the docstore.",
    ),
]

agent = ReActCustomAgent.from_llm_and_tools(
  llm,
  tools,
)

agent_executor = AgentExecutor.from_agent_and_tools(
  agent=agent,
  tools=tools,
  verbose=True,
)

さあ実際にこれを実行してみると以下のような形になります!実際に問い合わせ文が取れていますね!

question = "I would like to know the name of one of Darth Vader's son's associates."
agent_executor.run(question)


--------------

> Entering new AgentExecutor chain...
考察: I need to search Darth Vader's son and find one of his associates.
行動: Search[Darth Vader's son]
結果: Darth Vader is a fictional character in the Star Wars franchise. The character is the central antagonist of the original trilogy and, as Anakin Skywalker, is one of the main protagonists in the prequel trilogy. Star Wars creator George Lucas has collectively referred to the first six episodic films of the franchise as "the tragedy of Darth Vader". Darth Vader has become one of the most iconic villains in popular culture, and has been listed among the greatest villains and fictional characters ever. His masked face and helmet, in particular, is one of the most iconic character designs of all time.
Originally a slave on Tatooine, Anakin Skywalker is a Jedi prophesied to bring balance to the Force. He is lured to the dark side of the Force by Chancellor Palpatine (Darth Sidious) and becomes a Sith Lord, assuming the name of Darth Vader. After a lightsaber battle with his former mentor Obi-Wan Kenobi on Mustafar, in which he is severely injured, Vader is transformed into a cyborg. Now the Emperor's handpicked right-hand, he serves the Galactic Empire for over two decades as the Commander-in-Chief of its forces, purging the last Jedi and hunting the Rebels. Vader ultimately redeems himself by saving his son, Luke Skywalker, and killing Palpatine, sacrificing his own life in the process. He is also the secret husband of Padmé Amidala, the biological father of Princess Leia, and the grandfather of Kylo Ren (Ben Solo). In the non-canonical Star Wars Legends continuity, he is also the grandfather of Ben Skywalker, his eponym Anakin Solo, Jaina Solo and Darth Caedus (Jacen Solo), and the great-grandfather of Allana Solo.
The character has been portrayed by numerous actors: David Prowse physically portrayed Vader while James Earl Jones has voiced him in all of the films and some television shows, and Sebastian Shaw portrayed the unmasked Anakin in Return of the Jedi, as well as the character's spirit in the original release of that film. Jake Lloyd played Anakin Skywalker as a child in The Phantom Menace, the first film of the prequel trilogy, while Hayden Christensen played him as a young adult in the following two films, post-2004 releases of Return of the Jedi, and Obi-Wan Kenobi.
In addition to the first six Star Wars films, the character appears in the anthology film Rogue One. He also appears in television series (most substantially The Clone Wars) and numerous iterations of the Star Wars Expanded Universe, including video games, novels, and comic books. Due to Vader's popularity, various merchandise of the character, such as action figures and replicas of his lightsaber, has been produced.
考察: Darth Vader has a son, Luke Skywalker. I need to search Luke Skywalker and find one of his associates.
行動: Search[Luke Skywalker]
結果: Could not find [Luke Skywalker]. Similar: ['Luke Skywalker', 'Skywalker family', 'Darth Vader', 'Rey (Star Wars)', 'Mara Jade', 'Jedi', 'Chapter 16: The Rescue', 'Star Wars: From the Adventures of Luke Skywalker', 'Skellig Michael', 'Kylo Ren']
考察: To find one of Luke Skywalker's associates, I can search Luke Skywalker (Star Wars).
行動: Search[Luke Skywalker (Star Wars)]
結果: Luke Skywalker is a fictional character and the protagonist of the original film trilogy of the Star Wars franchise created by George Lucas. Portrayed by Mark Hamill, Luke first appeared in Star Wars (1977), and he returned in The Empire Strikes Back (1980) and Return of the Jedi (1983). Over three decades later, Hamill returned as Luke in the Star Wars sequel trilogy, cameoing in The Force Awakens (2015) before playing a major role in The Last Jedi (2017) and The Rise of Skywalker (2019). He later played a digitally de-aged version of the character in the Disney+ series The Mandalorian, appearing in the second-season finale, which premiered in 2020, and The Book of Boba Fett, in the sixth episode, released in 2022.
Originally a farmer on Tatooine living with his uncle and aunt, Luke becomes a pivotal figure in the Rebel Alliance's struggle against the Galactic Empire. The son of fallen Jedi Knight Anakin Skywalker (turned Sith Lord Darth Vader) and Padmé Amidala, Luke is the twin brother of Rebellion leader Princess Leia and eventual brother-in-law of the smuggler Han Solo. Luke trains to be a Jedi under Jedi Masters Obi-Wan Kenobi and Yoda and rebuilds the Jedi Order. He later trains his nephew Ben Solo and mentors Rey. Though Luke dies at the end of The Last Jedi, he returns as a Force spirit in The Rise of Skywalker, encouraging Rey to face her grandfather, the resurrected Emperor Palpatine. At the end of the film, the spirits of Luke and Leia give Rey their blessing to adopt the Skywalker surname and continue their family's legacy.
The character also briefly appears in the prequel film Episode III – Revenge of the Sith as an infant, portrayed by Aidan Barton, and in the Disney+ series Obi-Wan Kenobi as a child, portrayed by Grant Feely. In the de-canonized Star Wars Expanded Universe (renamed Legends), Luke is a main character in many stories set after Return of the Jedi, which depict him as a powerful Jedi Master, the husband of Mara Jade, father of Ben Skywalker, and maternal uncle of Jaina, Jacen and Anakin Solo.
考察: Luke Skywalker has many associates, such as Han Solo, Obi-Wan Kenobi, Yoda, Rey, and Ben Solo. So one of Darth Vader's son's associates is Han Solo.
完了: Finish[Han Solo]

> Finished chain.
Han Solo

以上いかがでしたでしょうか?ChatGPT自体は日々進化しておりWeb BrowserやPluginなど付与されていますが、Prompting Techniqueを駆使することによってその可能性をまた無限に広げることができそうなことを感じてもらえれば幸いです。ただ使う側からエンジニアとして一歩先行く取り組みになるようぜひ皆様もお試しください!

最後に宣伝です!

グループ研究開発本部の次世代システム研究室では、最新のテクノロジーを調査・検証しながらインターネットの新規サービスの開発を行うエンジニアを募集しています。募集職種一覧 からご応募をお待ちしています。

ブログの著者欄

グループ研究開発本部

GMOインターネットグループ株式会社

グループ研究開発本部とは、GMOインターネットグループの事業領域で力を入れているスタートアップやグループ横断のプロジェクトにおいて、技術支援・開発・解析などを行い、ビジネスの成功を支援する部署です。 最新のテクノロジーを研究開発し、いち早くビジネスに投入することで、お客様の発展と成功に貢献することを主要なミッションとしています。

採用情報

関連記事

KEYWORD

採用情報

SNS FOLLOW

GMOインターネットグループのSNSをフォローして最新情報をチェック