書籍『LangChainとLangGraphによるRAG・AIエージェント』をローカルで進める その2:LangfuseのPromptsとTracing

前回の記事で、LM Studioを利用してローカルでRAGを試してみました。

今回は、書籍でLangSmithが使われている部分について、ここもローカルで済ませるためにオープンソースのLangfuseとRAG評価フレームワークのRagasを組み合わせてみます。

LangSmithについて「書籍:LangChainとLangGraphによるRAG・AIエージェント 7.2 LangSmithの概要」には以下のように書いてあります。

LangSmithは大きく次の3種類の機能を持ちます。

1. Tracing (トレースの収集に関する機能)
2. Prompts (プロンプトの管理に関する機能)
3. Evaluation (評価に関する機能)

この3つの機能をLangfuseでやってみます。

終わってしまえば難しいところはないのですが、ちょいちょいハマったところがあったので、「⚠️」マークでメモを残しておきます。

Langfuseの準備

Langfuseのリポジトリからcloneして、docker-composeで起動するだけ。

$ docker compose up

localhost:3000にアクセス。sign upから適当なメールアドレスとパスワードでアカウントを作る。

「組織」作成後、設定画面でAPIキーを作る。

ここまででLangfuseの準備は完了!

Prompts(プロンプトの管理に関する機能)

LangfuseのPrompts機能を使ってみる。書籍の「4.3 Prompt template」に相当する。

この機能を使うと、LLMを使うアプリケーションのソースコードとは別にプロンプトの共有やバージョン管理ができる。

プロンプトをLangfuseに登録する

Langfuseの画面からプロンプトを作成する。作成後は以下のような画面。左メニューの「Prompts」を選択し、画面右上の「+ New prompt」から作成する。

作成画面で TextChat かを選択するところで Chat を選択する。そうすると以下のような入力フォームになる。システムプロンプトやユーザーのプロンプトを書ける。ここでは以下のように入力する。

  • system : ユーザーが入力した料理のレシピを考えてください。
  • user : {{dish}}

⚠️ ユーザープロンプトには {{dish}} とだけ書いてある。 {{ }} というの変数のプレースホルダーを表す。書籍で利用している LangSmith では { } がプレースホルダーらしい。最初は {dish} と書いていてうまく動かずハマった。LangSmithとLangfuseのライブラリの違いかな。

これで作成する。作成後、一覧画面からプロンプトを選択すると以下のような詳細画面になる。ここから編集ができるが、正確には編集ではなく新しいバージョンとして登録される。↓の画面では3つバージョンがある状態。

Langfuseに登録したプロンプトを使ってみよう

じゃあソースコードからこのプロンプトを使ってみる。

ライブラリを追加。

$ uv add langfuse==4.14.0

ソースコードは以下の通り。

⚠️ 書籍のLangSmithを使った例とほぼ同じなのだけど、 prompt.compile(dish="カレー") の部分が書籍では prompt.invoke({"dish": "カレー"}) と微妙に異なっていてハマった。

import os
from langfuse import Langfuse

os.environ["LANGFUSE_SECRET_KEY"] = "sk-..." # langfuseから取得
os.environ["LANGFUSE_PUBLIC_KEY"] = "pk-..." # langfuseから取得
os.environ["LANGFUSE_BASE_URL"] = "http://localhost:3000"
langfuse = Langfuse()

prompts_response = langfuse.api.prompts.list()

print("Langfuseに登録されてるプロンプト一覧")
for p in prompts_response.data:
    print(f"- '{p.name}' (Label: {p.labels})")

print("---")
prompt = langfuse.get_prompt("foo/recipe")
prompt_value = prompt.compile(dish="カレー")
print(prompt_value)

実行!ユーザープロンプトにプレースホルダーとして登録してあった {{dish}} が、 prompt.compile(dish=カレー) によって置き換わっていることが分かる。

$ uv run langfuse_prompt.py
Langfuseに登録されてるプロンプト一覧
- 'foo/recipe' (Label: ['latest', 'production'])
---
[{'role': 'system', 'content': 'ユーザーが入力した料理のレシピを考えてください。'}, {'role': 'user', 'content': 'カレー'}

Tracing (トレースの収集に関する機能)

次はTracingを試す。書籍の「5.1 Runnbable と RunnableSequence - LCELの最も基本的な構成要素」に相当する。

LangChain の LCEL( | で繋ぐ記法)で実装する。 invoke() にLangfuseの CallbackHandler を渡すだけでLangfuseの画面で処理をトレースできる。

⚠️ | で繋げられるようにLangfuseから取得したpromptを変換する。

# Prompt Template
import os
from langfuse import Langfuse
from langchain_core.prompts import ChatPromptTemplate

os.environ["LANGFUSE_SECRET_KEY"] = "sk-..." # langfuseから取得
os.environ["LANGFUSE_PUBLIC_KEY"] = "pk-..." # langfuseから取得
os.environ["LANGFUSE_BASE_URL"] = "http://localhost:3000"
langfuse = Langfuse()
langfuse_prompt = langfuse.get_prompt("foo/recipe")
prompt_messages = langfuse_prompt.get_langchain_prompt() # プロンプトのListを取得する
prompt = ChatPromptTemplate.from_messages(prompt_messages) # LangChain用のPromptTemplateに変換する



# Chat model
from langchain_openai import ChatOpenAI
model = ChatOpenAI(
    base_url = "http://localhost:1234/v1",
    api_key = "dummy",
)



# Output Parser
from langchain_core.output_parsers import StrOutputParser
output_parser = StrOutputParser()


# チェイン
chain = prompt | model | output_parser


# LangfuseのTracing
from langfuse.langchain import CallbackHandler
callback_handler = CallbackHandler()


# 実行
output = chain.invoke({"dish": "カレー"}, config={"callbacks": [callback_handler]})
print(output)

LM Studioのサーバーを起動させ(モデルは openai/gpt-oss-20b をLoadした)、実行してみる!と、Langfuseの画面、左メニューの「Tracing」から見れた!書籍では色々なチェインの組み方でRAGを使う処理を工夫するのだけど、Tracingで見れるのは楽しかった。

ちなみに出力は以下の通り。しっかりカレーのレシピが出ました。

% uv run lcel_langfuse.py
## ざっくりした日本風カレーのレシピ

| ステップ | 内容 |
|--------|------|
| 1. 下ごしらえ | 野菜・肉を切る |
| 2. カレールー作り | 油で炒めてからルーを入れる |
| 3. 煮込み | 水・コク材でじっくり煮る |
| 4. 盛り付け | ご飯とともに仕上げ |

---

### 1️⃣ 下ごしらえ(10分)

| 食材 | 量 | カット例 |
|------|----|----------|
| 鶏もも肉(または牛ひき肉) | 300 g | 1.5 cm角 |
| 玉ねぎ | 2個 | 薄切り |
| にんじん | 1本 | 斜め薄切り |
| ジャガイモ | 2個 | 1.5 cm角 |
| ピーマン | 1個 | くし切り |
| しいたけ | 4枚 | スライス |
| ニンニク | 1片 | みじん切り |
| 生姜 | 1片 | みじん切り |

> **ポイント**
> - 玉ねぎは焦げないように、弱火でじっくり炒めると甘みが出ます。
> - 皮をむいたジャガイモは水にさらしておくと、余分なでんぷんが落ちてテクスチャーが良くなります。

---

### 2️⃣ カレールー作り(15分)

1. **フライパンに油**
   鶏肉または牛ひき肉を入れ、表面が白っぽくなるまで中火で炒める。
   *(牛ひき肉の場合は、余分な脂を軽く切り落とす)*

2. **野菜を加える**
   玉ねぎ、にんじん、ジャガイモ、ピーマン、しいたけを順に加え、5分ほど炒める。
   *玉ねぎが透き通って甘みが出たら次へ。*

3. **にんにく・生姜を投入**
   1〜2分炒めて香りが立ったら、次にカレールーを入れる。
   ここで **市販のルウ**(カレー粉+小麦粉ベース)を使うと簡単。
   - ルーは **10 g × 3〜4個**(約30–40 g)を目安。
   - ルーの粒が大きい場合は、**少量の熱湯で溶かしてから加えると滑らかに仕上がります。**

4. **水を注ぐ**
   ルーが完全に溶けたら、**1.5 Lの水**(またはチキンブイヨン)を加え、全体を混ぜる。
   *水の量はお好みで調整可能です(濃いめにしたいなら少なめ)。*

---

### 3️⃣ 煮込み(20〜30分)

1. **蓋をして弱火**
   15分ほど煮込み、野菜が柔らかくなるまで待つ。
   - 途中で**火を強めて沸騰させ、アク抜き**も忘れずに。

2. **コクを加える**
   - みりん大さじ1、醤油小さじ2、塩少々(約5 g)を加える。
   - さらに**牛乳または生クリーム大さじ3**を加えるとまろやかになります。
   - お好みで **カレー粉大さじ1** も追加してスパイシーに。

3. **仕上げの味付け**
   - 味が足りない場合は、砂糖小さじ1〜2で甘味を調整。
   - **レモン汁小さじ1** で酸味を加えると、風味が引き締まります。

---

### 4️⃣ 盛り付け(5分)

1. **ご飯**
   - 蒸し米を皿に盛り、カレーを上からかける。
   - ご飯は**白米**でも、**玄米**や**雑穀米**でも◎。
2. **トッピング**(任意)
   - パクチー、ゆで卵の黄身を割り入れる。
   - **ピクルス**や**カレーパン**を添えると、味のコントラストが楽しめます。

---

## さらにアレンジしたい方へ

| アレンジ | 方法 |
|----------|------|
| **ベジタリアン** | 肉を外し、豆(レンズ豆やひよこ豆)を加える。 |
| **低糖質** | ジャガイモの代わりにカリフラワーを使用。 |
| **スパイシー** | ターメリック・カイエンペッパーを大さじ1ずつ追加。 |
| **フルーツ風味** | りんごのピューレを大さじ2入れ、甘みと酸味を追加。 |

---

### まとめ

- **下ごしらえ**で食材の味を引き出すことが重要。
- **ルーの溶かし方**を工夫すると、ムラなく滑らかなカレーに仕上がります。
- **最後の味付け**で自分好みの濃さ・スパイシーさを調整できます。

これで、誰でも手軽に作れる本格的な日本風カレーが完成です!ぜひお試しください。

おしまい

LangfuseのPrompts機能を使ってGUIで管理したプロンプトを、そのままローカルLLMに渡し、その実行ログをLangfuseのTracing機能で確認することができました!

次回は、Langfuseの「Evaluation(評価に関する機能)」を使ってRAGの評価に挑みます。