Closed5

langgraph0.2ドキュメントを読む

cygkichicygkichi

https://langchain-ai.github.io/langgraph/

graph_mermaid
シンプルなグラフ。

code
定義されたツール(search)は、modelにバインドされる。
bind_toolsは、入力に基づいてツールを使用する必要があるならば使い構造的な出力がなされ、必要がないならば使われず通常の会話が返答される。

agentノードでmodelが呼び出され、条件付きエッジでshould_continueによりツールの入力が生成されたかを判断し、生成されているならばtoolノードに、そうでないならばendノードに遷移する。

result
天気を聞いているときは、ツールに記述された90度という回答。ちゃんと分岐していそう。

cygkichicygkichi

https://langchain-ai.github.io/langgraph/tutorials/introduction/

Part 1: Build a Basic Chatbot

graph_01
chatbotノードは単純にllmを呼び出すだけのもの。
無限ループで会話できているように見えるが、endノードに到達するたびにstateの情報(会話履歴)をリセットしている点は注意。

Part 2: Enhancing the Chatbot with Tools¶

basictoolnode
ツールノードの中身。

  • ツールノードのtool_by_nameというプロパティにツールの名前をキーにしてツールを格納している
  • 呼び出されるとき(call)の引数inputsはエージェントの状態stateが入るのか?
  • そうならば、会話履歴の最後のメッセージがmessageに入り、このメッセージはtoolを使う指示が入っているはず。
  • message.tool_callsに使用するツール名称が記載されている。普通の会話にはtool_callsという情報はないのだろう。
  • そしてその指示されたツールを選択して、ツールにinvokeされ、 応答がToolMessageに入り、会話履歴に追加される。
  • ツールノードの戻り値はほかのノードと同じく、エージェントの状態を更新するような形

tools_condition

  • このroute_toolsというのは、tools_conditionというの中身らしい。

https://langchain-ai.github.io/langgraph/reference/prebuilt/#tools_condition

  • tools_conditionのドキュメントをみると、toolsという名前でノードを定義しておく必要があるらしい。
  • ツールノードは一つしか定義で来なさそうだし、使い勝手悪そう。
  • 逆にツールノードはひとつにまとめておくのがベストプラクティスなのかもしれない。

Part 3: Adding Memory to the Chatbot

from langgraph.checkpoint.memory import MemorySaver
memory = MemorySaver()
# 略
graph = graph_builder.compile(checkpointer=memory)

こんな感じでグラフにチェックポイント(メモリ)を追加して、、

config = {"configurable": {"thread_id": "1"}}
events = graph.stream(
    {"messages": [("user", user_input)]}, config, stream_mode="values"
)
  • 呼び出すときに、スレッドIDを追加すると、endノードに到達してもエージェントの状態(会話履歴)を呼び出せる。
  • stream_modeは、確か応答の形式だった気がする。ややこしいのでここではそういうもんとして飛ばす

Part 4: Human-in-the-loop

graph = graph_builder.compile(
    checkpointer=memory,
    # This is new!
    interrupt_before=["tools"],
    # Note: can also interrupt __after__ tools, if desired.
    # interrupt_after=["tools"]
)

このように、ノードの前後でlanggraphの動作を中断して、人間による介入ができる。
ただ、このサンプルコードでは中断して状態を確認しているだけで、介入はしていない。
介入は次のPart5で説明されるようだ。

Part 5: Manually Updating the State

graph.update_state(
    # Which state to update
    config,
    # The updated values to provide. The messages in our `State` are "append-only", meaning this will be appended
    # to the existing state. We will review how to update existing messages in the next section!
    {"messages": new_messages},
)

Part4で中断方法を学んだ。
ここでは中断したときの介入方法を説明している。
具体的には介入時に会話履歴を更新(追記)している。

graph.update_state(
    config,
    {"messages": [AIMessage(content="I'm an AI expert!")]},
    # Which node for this function to act as. It will automatically continue
    # processing as if this node just ran.
    as_node="chatbot",
)

as_nodeでchatbotノードで処理したことになるっぽいけど、よくわからんなぁ。

from langchain_core.messages import AIMessage

snapshot = graph.get_state(config)
existing_message = snapshot.values["messages"][-1]
print("Original")
print("Message ID", existing_message.id)
print(existing_message.tool_calls[0])
new_tool_call = existing_message.tool_calls[0].copy()
new_tool_call["args"]["query"] = "LangGraph human-in-the-loop workflow"
new_message = AIMessage(
    content=existing_message.content,
    tool_calls=[new_tool_call],
    # Important! The ID is how LangGraph knows to REPLACE the message in the state rather than APPEND this messages
    id=existing_message.id,
)

print("Updated")
print(new_message.tool_calls[0])
print("Message ID", new_message.id)
graph.update_state(config, {"messages": [new_message]})

print("\n\nTool calls")
graph.get_state(config).values["messages"][-1].tool_calls

会話履歴を追加ではなく、上書きする方法。
AIMessageのidを同じものにすると上書きされるらしい。

cygkichicygkichi

https://langchain-ai.github.io/langgraph/tutorials/introduction

Part 6: Customizing State

class State(TypedDict):
    messages: Annotated[list, add_messages]
    # This flag is new
    ask_human: bool

このように、人間に頼る必要があるかどうかはエージェントの状態に記載する。ルーターに判断させるのではなく、一時的に状態に保持しておくのか。。

from langchain_core.pydantic_v1 import BaseModel
class RequestAssistance(BaseModel):
    """Escalate the conversation to an expert. Use this if you are unable to assist directly or if the user requires support beyond your permissions.
    To use this function, relay the user's 'request' so the expert can provide the right guidance.
    """
    request: str
# 略
llm_with_tools = llm.bind_tools(tools + [RequestAssistance])

こうするとRequestAssistanceという名称のツールとして設定される。

from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o")
llm.bind_tools([RequestAssistance]).invoke("専門家に狸について聞いて下さい。")
#AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_aCHjFISASGsWoNB5JAmLqT4p', 'function': {'arguments': '{"request":"狸について質問があります。お答えいただけますか?"}', 'name': 'RequestAssistance'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 27, 'prompt_tokens': 95, 'total_tokens': 122}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_157b3831f5', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-08284deb-35d7-4aa1-a808-e2a662b71d36-0', tool_calls=[{'name': 'RequestAssistance', 'args': {'request': '狸について質問があります。お答えいただけますか?'}, 'id': 'call_aCHjFISASGsWoNB5JAmLqT4p', 'type': 'tool_call'}], usage_metadata={'input_tokens': 95, 'output_tokens': 27, 'total_tokens': 122})

専門家に狸について聞くようなrequestが記載された応答が乗っている。このrequestの「狸について質問があります。お答えいただけますか?」が人間への質問となる。

def chatbot(state: State):
    response = llm_with_tools.invoke(state["messages"])
    ask_human = False
    if (
        response.tool_calls
        and response.tool_calls[0]["name"] == RequestAssistance.__name__
    ):
        ask_human = True
    return {"messages": [response], "ask_human": ask_human}

from langchain_core.messages import AIMessage, ToolMessage


def create_response(response: str, ai_message: AIMessage):
    return ToolMessage(
        content=response,
        tool_call_id=ai_message.tool_calls[0]["id"],
    )


def human_node(state: State):
    new_messages = []
    if not isinstance(state["messages"][-1], ToolMessage):
        # Typically, the user will have updated the state during the interrupt.
        # If they choose not to, we will include a placeholder ToolMessage to
        # let the LLM continue.
        new_messages.append(
            create_response("No response from human.", state["messages"][-1])
        )
    return {
        # Append the new messages
        "messages": new_messages,
        # Unset the flag
        "ask_human": False,
    }

graph_06

  • 会話履歴を解釈して、専門家に聞いたほうが良さそう(humanツールを使用したほうが良さそう)な場合ask_humanをTrueにする。
  • chatbotから次にノードを決める、条件付きエッジでask_humanがTrueならhumanノードに遷移する。
  • humanノードはinterupt_beforeされているため、ここで人間回答が求められる。
  • humanノードではToolMessageとして文章を入力するらしい。(これはHumanMessageかと思ってた。)
cygkichicygkichi

https://langchain-ai.github.io/langgraph/tutorials/introduction/#part-7-time-travel

Part 7: Time Travel

to_replay = None
for state in graph.get_state_history(config):
    print("Num Messages: ", len(state.values["messages"]), "Next: ", state.next)
    print("-" * 80)
    if len(state.values["messages"]) == 6:
        # We are somewhat arbitrarily selecting a specific state based on the number of chat messages in the state.
        to_replay = state

get_state_historyで過去の状態を呼びだせる。

print(to_replay.next)
print(to_replay.config)
#('action',)
#{'configurable': {'thread_id': '1', 'thread_ts': '2024-05-06T22:33:10.211424+00:00'}}

# The `thread_ts` in the `to_replay.config` corresponds to a state we've persisted to our checkpointer.
for event in graph.stream(None, to_replay.config, stream_mode="values"):
    if "messages" in event:
        event["messages"][-1].pretty_print()

過去の状態を入力すれば、過去の状態からエージェントを実行できるらしい。
thread_idが同じだと現在の状態を引き継いで実行されるのだが、thread_tsがあるとうまいことあるポイント以降からエージェントが実行されるらしい。

cygkichicygkichi

https://langchain-ai.github.io/langgraph/tutorials/chatbots/information-gather-prompting/

Prompt Generation from User Requirements

graph_image
ユーザーがプロンプトを生成するのを支援するチャットボット。

info

template = """あなたの仕事は、作成したいプロンプトテンプレートの種類に関する情報をユーザーから取得することです。

ユーザーから以下の情報を得る必要があります:

- プロンプトの目的
- プロンプトテンプレートに渡す変数
- 出力が行うべきでない制約
- 出力が守らなければならない要件

もしこれらの情報がわからない場合は、明確にしてもらうこと!乱暴に推測しないでください。

すべての情報を特定できたら、関連するツールを呼び出すこと。」"""

def get_messages_info(messages):
    return [SystemMessage(content=template)] + messages

class PromptInstructions(BaseModel):
    """Instructions on how to prompt the LLM."""
    objective: str
    variables: List[str]
    constraints: List[str]
    requirements: List[str]

llm = ChatOpenAI(temperature=0)
llm_with_tool = llm.bind_tools([PromptInstructions])

def info_chain(state):
    messages = get_messages_info(state["messages"])
    response = llm_with_tool.invoke(messages)
    return {"messages": [response]}

まずはinfoノードの実装。
会話履歴(人間の指示を含む)をもとに、PromptInstructionsで定義された出力で出力する。
人間の指示がプロンプトの生成と関係ない場合は、通常の会話がなされる。
messagesには構造化出力をそのまま入れる。''このAIMessageの会話文は空。content=''

from typing import Literal
from langgraph.graph import END

def get_state(state) -> Literal["add_tool_message", "info", "__end__"]:
    messages = state["messages"]
    if isinstance(messages[-1], AIMessage) and messages[-1].tool_calls:
        return "add_tool_message"
    elif not isinstance(messages[-1], HumanMessage):
        return END
    return "info"

infoノードの次は、このget_stateによって分岐が指示される。PromptInstructionsツールが指定されている場合はツールに、通常の会話メッセージの場合は終了

add_tool_message

@workflow.add_node
def add_tool_message(state: State):
    return {
        "messages": [
            ToolMessage(
                content="Prompt generated!",
                tool_call_id=state["messages"][-1].tool_calls[0]["id"],
            )
        ]
    }

add_tool_messageノードの中身はこれ。
前のAIメッセージのcontentは空で、ツールへの指示だけが指定されていた。
ツールへの指示をToolMessageとしてここで記入する。

prompt

from langchain_core.messages import AIMessage, HumanMessage, ToolMessage

# New system prompt
prompt_system = """Based on the following requirements, write a good prompt template:

{reqs}"""

# Function to get the messages for the prompt
# Will only get messages AFTER the tool call
def get_prompt_messages(messages: list):
    tool_call = None
    other_msgs = []
    for m in messages:
        if isinstance(m, AIMessage) and m.tool_calls:
            tool_call = m.tool_calls[0]["args"]
        elif isinstance(m, ToolMessage):
            continue
        elif tool_call is not None:
            other_msgs.append(m)
    return [SystemMessage(content=prompt_system.format(reqs=tool_call))] + other_msgs

def prompt_gen_chain(state):
    messages = get_prompt_messages(state["messages"])
    response = llm.invoke(messages)
    return {"messages": [response]}
  • 最後にプロンプトノードの中身(prompt_gen_chain)。
  • get_prompt_messagesで会話履歴を整頓する。
    • まず、最初にシステムメッセージを入れる。
    • その後過去の会話履歴の中から、ツールメッセージとツールを呼び出すAIメッセージを除外した会話履歴、つまり普通の人の会話とAIの会話を追記する。
    • システムプロンプトのreqsには、ツールの中身を記載する
    • (コメント)もっと良さそうな実装ありそう。見てるだけだと。
  • チュートリアルの最後だと、end→startと無限ループとすることで人間の入力を求めている。
    • ここで制約とかを追記できるし、意味不明なことをいうと通常の対話で聞き返される。
このスクラップは2025/01/04にクローズされました