1. きっかけ
このイベントに参加したんです。昼休みだし。昼休みはいつも自席でパン食べてるし参加しやすい。
それに、 LangMemってなんや! って気になったし。
これだけのきっかけだったのですが、会話の中から記憶を追加していくって使う場面が簡単に想定できたんです。wkwk
僕、自転車のロードレースの中継をしながらしゃべったりするんですけど、レース時間が長いので序盤のレース展開とか簡単に忘れちゃう(ぉい!)。そして思い出せない。(加齢)
そこで、このLangMemを使うと解決でき・・・そうじゃん!
わくわくしちゃう
わくわくはモチベーションでしょ!!
で、LangMem
はLanggraph
のPrebuilt
のなかのAgent
の一つであったりします。
Agent
のページからLangMem
に行くとGitHub
に行っちゃいますが、以下から行くとドキュメントに行けます。こっちから行くとドキュメントにいって、あっちから行くとGitHubってめんどくさい。もう少しわかりやすくしてくれると良いのにね!笑
2. バージョン情報
バージョン情報
Python==3.11.9
langchain==0.3.19
python-dotenv
langchain-openai==0.3.7
langgraph==0.3.2
langchain-core==0.3.40
langchain-community==0.3.18
実はいろいろあって、Pythonのバージョンをあげちゃいました。
そして、このLangmemをやるためにはlanggraphのバージョンも変更する必要があり、いろいろとこれまでのバージョンと変わっていますのでご注意ください。
実はイベントの時にも主催のGenerativeAgentsの大嶋さんもおっしゃられていたのですが、イベントの日は最新バージョンでは動きませんでした。なんとその日にアップデートしたlanggraphがダメだったという。www
Pypiを見ると、アップデートに苦労したみたいデス。
そして、使いたいlanggraph-prebuild
が「だめだからおすすめしないよ!」ってなってますね。
そして、0.3.1でもまともに動かず、0.3.2になるとちゃんと使えるようになりました。
ちょっと忙しくて、放置してたらどんどんアップデートされたので無駄な努力をしなくて済んだので良かった。
それにしても、大嶋さん、YoutubeLive当日にアップデートがあって使えなくなるとか引きが強い。しかし、焦りも、きょどりもない!カコエエ
(見習いたい)
3. 今回はここをやってみよう
ざっとドキュメントに目を通してみて(読んではない)、ここをやってみることにしましょう。
理由は
- LangChain、LangGraphをここまでやってきているならわかるはず
- 慣れてるOpenAIだし
ドキュメント通りでは動かなかったので、ちょっと改造してみました。
4. プログラム
langmemはpip install langmem==0.3.2
でインストールしちゃいます。
はい、僕はpip信者です。でも、イベントの中でuv
使っててちょっとよさげって思った。
4-1. ライブラリインポート
import os
from typing import Any, Dict, List
from dotenv import load_dotenv
from langchain_openai import AzureOpenAI, AzureChatOpenAI
from langgraph.store.memory import InMemoryStore
from langmem import create_manage_memory_tool, create_search_memory_tool
4-2. 環境変数の設定
load_dotenv()
os.environ['AZURE_OPENAI_API_KEY'] = os.getenv('API_KEY')
os.environ['AZURE_OPENAI_ENDPOINT'] = os.getenv('END_POINT')
今回は環境変数にAPIキーやエンドポイントを設定します。
これは環境変数に入れることでしか対応できません。ライブラリのクラスを書き換えればできると思いますが、あんま、メリットないので、サクッと環境変数に入れておきます。
4-3. tools実行関数
toolsの実行関数を作ります。
そう、Langmemはtoolsとして実装します。
def execute_tool(tools_by_name: Dict[str, Any], tool_call: Dict[str, Any]) -> str:
"""Execute a tool call and return the result"""
tool_name = tool_call["name"]
if tool_name not in tools_by_name:
return f"Error: Tool {tool_name} not found"
else:
# print(tool_call)
pass
tool = tools_by_name[tool_name]
# print(tool)
try:
result = tool.invoke(tool_call["args"])
return str(result)
except Exception as e:
return f"Error executing {tool_name}: {str(e)}"
ほぼ、ドキュメントのコピペです。受け渡されるtool_callingとtool_calling結果で関数を実行するための関数です。
keyはtool名、valueは関数のtoolsの辞書を受けとって、実行すべきtoolsを受け取ることで実行できるようになっています。このtoolsにはメモリーとして保持すべき内容を保存するtoolとメモリーから情報を引き出すtoolを実装することになります。
他のtoolsも利用可能な汎用的な関数ですね。
変更したのは引数のtool_call
に'function'
がない渡し方をしたので、僕の関数からは取り除いています。
4-4. Agent実行関数
def run_agent(tools: List[Any], user_input: str, max_steps: int = 5) -> str:
"""Run a simple agent loop that can use tools"""
# for Args of execute_tool function
tools_by_name = {tool.name: tool for tool in tools}
# Convert tools to OpenAI's format
openai_tools = [
{
"type": "function",
"function": {
"name": tool.name,
"description": tool.description,
"parameters": tool.tool_call_schema.model_json_schema(),
},
}
for tool in tools
]
# Setup
llm = AzureChatOpenAI(
model="gpt-4o-mini",
azure_endpoint=os.getenv("END_POINT"),
api_version=os.getenv("API_VERSION"),
api_key=os.getenv("API_KEY")
)
llm_with_tools = llm.bind_tools(openai_tools)
# Prepare 1st Messages
messages = [{"role": "user", "content": user_input}]
# REACT loop
for _ in range(max_steps):
response = llm_with_tools.invoke(messages)
tool_calls = response.tool_calls
if not tool_calls:
# No more tools to call, return the final response
# print('no tools')
return response.content
# append result to messages
messages.append(
{"role": "assistant", "content": response.content, "tool_calls": tool_calls}
)
# calling and execute tools
for tool_call in tool_calls:
# print(tool_call)
tool_result = execute_tool(tools_by_name, tool_call)
# append tool call result in tool cooling format
messages.append(
{
"role": "tool",
"tool_call_id": tool_call["id"],
"content": tool_result,
}
)
return "Reached maximum number of steps"
おおむねコピペです。
流れをざっくり解説すると、
-
execute_tool
関数の引数のtools_by_nameを用意 -
tool
sをOpenAIフォーマットに作り直し - GPT-4o-miniを用意して、さらに、
tools
をバインド - 最初のmessagesを用意
- 決めた回数(今回は5回)、
messages
をバインドモデルに入力(出力はmessages
に追加) - 出力に
tool_calling
されていたらexecute_tool
を実行 -
tool_calling
がなければ、モデルの出力をそのまま出力
という流れ。
4-5. toolsの用意
# Set up memory store and tools
store = InMemoryStore(
index={
"dims": 1536,
"embed": "azure_openai:text-embedding-ada-002",
}
)
memory_tools = [
create_manage_memory_tool(namespace="memories", store=store),
create_search_memory_tool(namespace="memories", store=store),
]
storeに、InMemoryStoreを用います。コンピュータのメモリにベクトルストアを作る感じのようです。ここで、embeddingモデルを呼ぶんですが、ここでapi_keyなどを渡す方法を見つけられませんでした。
4-2
でapi_keyなどを環境変数に入れておいた理由はこれです。
そして、ここでlangmemの関数が二つ出てきます。
create_manage_memory_tool: メモリーに書き込んだり、アップデートしたり、消したりするツール
create_search_memory_tool: 検索してメモリーから情報を取り出すツール
この二つをtoolsとして用います。
namespaceは適当に決めて良いと思いますが、のちにstoreから検索するときにnamespaceを渡すので、整合は取りましょう。
4-6. 実行
result = run_agent(
tools=memory_tools,
user_input="僕はフィドルを弾くのが好きですが、他人の誹謗中傷を聞くのは嫌いなので、覚えておいてください。",
)
print(result)
# フィドルを弾くのが好きで、他人の誹謗中傷を聞くのが嫌いということを覚えました。何か他に覚えておいてほしいことがあれば教えてください!
フィドルを弾くのが好きだけど、誹謗中傷を聞くのは嫌だよ。覚えておいてね!って入力しました。
その出力として、「覚えといたよ~」と帰ってきましたね。
4-6. storeの確認
print(store.search(("memories",)))
# [
# Item(
# namespace=['memories'],
# key='2a983e27-5a06-435f-b0ac-2d9dab09030f',
# value={'content': 'ユーザーはフィドルを弾くのが好きです。'},
# created_at='2025-03-04T20:49:19.439791+00:00',
# updated_at='2025-03-04T20:49:19.439791+00:00', score=None
# ),
# Item(
# namespace=['memories'],
# key='d638dfe8-2ac2-41ce-bcdc-1aa1e24855fd',
# value={'content': 'ユーザーは他人の誹謗中傷を聞くのが嫌いです。'},
# created_at='2025-03-04T20:49:19.490007+00:00',
# updated_at='2025-03-04T20:49:19.490007+00:00',
# score=None
# )
# ]
4-6.メモリーから情報を取得しながら回答
4-6-1. 嫌いなことを聞いてみる
response = run_agent(
tools=memory_tools,
user_input='ところで、僕が嫌いなのはなんだっけ?'
)
print(response)
# あなたが嫌いなのは、他人の誹謗中傷を聞くことです。
そうだよ。みんな、SNSでの誹謗中傷はやめようね!
4-6-2. 好きなことを聞いてみる
response = run_agent(
tools=memory_tools,
user_input='僕の好きなことはなんだっけ?'
)
print(response)
# 'あなたが好きなことは、フィドルを弾くことです。また、他人の誹謗中傷を聞くのが嫌いなことも覚えています。他に何か好きなことがあれば教えてください!'
うまく弾けないけど、フィドルはたのしいんだよ。
4-6-3. 情報を追加する
response = run_agent(
tools=memory_tools,
user_input="今日は家に帰ったらフィドルの練習しなきゃ。Mason's Apronの練習にしよう"
)
print(response)
# フィドルの練習、楽しんでくださいね!Mason's Apronは素敵な曲ですね。頑張ってください!
そうそう、いい曲なんだよ。
4-6-4. さらに質問する
response = run_agent(
tools=memory_tools,
user_input="家に帰ったら何を練習するんだっけ?"
)
print(response)
# 家に帰ったら、Mason's Apronをフィドルの練習をする予定です。楽しんでくださいね!
おおお、これは便利!
記憶力ゼロのぼくには常備したいくらいだ。w
5. おわりに
InMemoryStoreとtoolsを組み合わせて会話内容から必要な内容を一時メモリに保存しておく方法をためしてみました。こうすると、モデルに入力するトークン数を抑えたり、messagesに入れておく会話回数を小さくしたりすることができるのではないでしょうか。
また、ベクトルストアを自然な対話の中から構築することができるので、俺のベクターストアが自然とできることになります。
それにしてもわくわくするな。これ
とはいえ、ひと様のプログラムのコピペなんで、いまいち理解が薄い気がする。
(ほんとは言語モデル開発が止まっているので再開したい。)