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

シンプルなグラフ。
定義されたツール(search)は、modelにバインドされる。
bind_tools
は、入力に基づいてツールを使用する必要があるならば使い構造的な出力がなされ、必要がないならば使われず通常の会話が返答される。
agentノードでmodelが呼び出され、条件付きエッジでshould_continue
によりツールの入力が生成されたかを判断し、生成されているならばtoolノードに、そうでないならばendノードに遷移する。
天気を聞いているときは、ツールに記述された90度という回答。ちゃんと分岐していそう。

Part 1: Build a Basic Chatbot
chatbotノードは単純にllmを呼び出すだけのもの。
無限ループで会話できているように見えるが、endノードに到達するたびにstateの情報(会話履歴)をリセットしている点は注意。
Part 2: Enhancing the Chatbot with Tools¶
ツールノードの中身。
- ツールノードの
tool_by_name
というプロパティにツールの名前をキーにしてツールを格納している - 呼び出されるとき(call)の引数
inputs
はエージェントの状態state
が入るのか? - そうならば、会話履歴の最後のメッセージがmessageに入り、このメッセージはtoolを使う指示が入っているはず。
-
message.tool_calls
に使用するツール名称が記載されている。普通の会話にはtool_calls
という情報はないのだろう。 - そしてその指示されたツールを選択して、ツールにinvokeされ、 応答がToolMessageに入り、会話履歴に追加される。
- ツールノードの戻り値はほかのノードと同じく、エージェントの状態を更新するような形
- この
route_tools
というのは、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を同じものにすると上書きされるらしい。

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

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
があるとうまいことあるポイント以降からエージェントが実行されるらしい。

Prompt Generation from User Requirements
ユーザーがプロンプトを生成するのを支援するチャットボット。
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と無限ループとすることで人間の入力を求めている。
- ここで制約とかを追記できるし、意味不明なことをいうと通常の対話で聞き返される。