0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LangChain LangGraph LangSmith 入門① 基礎編

0
Posted at

本記事より AI エージェント関連、特に LangChain 関連についてお話していきたいと思います。
内容が多いので、基礎的な概念や説明の基礎編、具体的な実例を交えての応用編と、複数記事に分ける予定です。
ご了承ください。

参考:

  • LangChainとLangGraphによるRAG・AIエージェント[実践]入門

  • 【AIエージェント開発】LangChainとLangGraphの違いについて📝

LangChain とは

現在主流の MIT ライセンスの AI エージェント開発のフレームワーク。

公式の実装では Python と JavaScript/TypeScript の 2 種類が提供されている。

Python 界隈での討論もライブラリの更新頻度も高く、比較的早い段階から商業利用されてきた実績があるため、今後の AI エージェント開発の変化や、様々な導入場面への対応に強いと考えられる。

LangGraph とは

LangChain の拡張ライブラリ、グラフ構造を採用し、ループや分岐・条件付きフローの設計が可能。

永続化、リトライ、Human in the loop、状態管理による会話の継続や途中再開が可能

LangGraph Platform により UI から視覚的にフロー設計が可能。

LangChain の全体像

パッケージ群(一例)

|-Packages-----------------|
| LangChain-Core           |
| LangChain-Community      |
| LangChain-text-Splitters | 
| LangChain-Experimental   |
|                          |
|-Partners------------|    |
| LangChain-OpenAI    |    |
| Langchain_Anthropic |    |
|---------------------|    |
|--------------------------|

|-Ecosystem-|
| LangSmith |
| LangServe |
| LangGraph |
|-----------|

主要コンポーネント集

コンポーネント 説明
Chat Model LLM とのインテグレーション
Prompt Template プロンプトテンプレート
Example Selector Few-shot プロンプトの例を動的に選択
Output Parser 言語モデルの出力を指定した形式に変更
Chain 各種コンポーネントを使った処理の連鎖
Document Loader データソースからドキュメントを読み込む
Document Transformer ドキュメントに変更を加える
Embedding Model ドキュメントのベクトル化
Vector Store ベクトル化したドキュメントの保存先
Retriever 入力と関連するドキュメントを検索
Tool Function Calling などでモデルが使用する関数を抽象化
Toolkit 同時に使用することを想定した Tool コレクション
Chat History 会話履歴の保存先としての各データベースとのインテグレーション

LangChain のインストール

mkdir test_agent
uv init
uv add langchain-core langchain-community langchain-anthropic

最小限のコードで LangChain から LLM をコールしてみる

参考:

__init__.py

import os
from langchain_anthropic import ChatAnthropic

def main():
    chat = ChatAnthropic(
        model="claude-haiku-4-5-20251001",
        anthropic_api_key=os.getenv("ANTHROPIC_API_KEY")
    )

    response = chat.invoke("1+1=?")
    print(response.content)
uv run test-agent

レスポンス

1 + 1 = 2

Chat Model

langchain-anthropic は Chat Model と呼ばれており、チャット形式のやり取りを入力して応答を得るようになっています。

import os
from langchain_anthropic import ChatAnthropic
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage

def main():
    model = ChatAnthropic(
        model="claude-haiku-4-5-20251001",
        anthropic_api_key=os.getenv("ANTHROPIC_API_KEY"),
        temperature=0
    )

    messages = [
        SystemMessage("You are a helpful assistant."),
        HumanMessage("Hi, my name is Jhon."),
        AIMessage("Hello Jhon! How can I help you?"),
        HumanMessage("Do you know my name?")
    ]

    ai_message = model.invoke(messages)
    print(ai_message.content)

レスポンス

Yes, you told me your name is Jhon.
I can see it from our conversation history.
Nice to meet you, Jhon!

Streaming

スリーミング形式で表示させたい場合はコールと表示の部分を

ai_message = model.invoke(messages)
print(ai_message.content)

以下に置き換える

for chunk in model.stream(messages):
    print(chunk.content, end="", flush=True)

Prompt Template

Prompt の扱いを抽象化したコンポーネントです。

LLM をコールせずにコンポーネントだけ動作確認:

from langchain_core.prompts import PromptTemplate

prompt = PromptTemplate.from_template(
"""
Where is the capital of {Country_Name}
""")

prompt_value = prompt.invoke({"Country_Name": "US"})
print(prompt_value.text)

—> Where is the capital of US

ChatPromptTemplate

上記にて紹介した AIMessage, HumanMessage, SystemMessage
をそれぞれテンプレート化して、ChatPromptTemplate クラスでまとめることができます。

from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages(
    [
        ("system", "Please tell capital city of the country which user is asking"),
        ("human", "{Country_Name}"),
    ])

prompt_value = prompt.invoke({"Country_Name": "US"})
print(prompt_value)

—>

messages=[SystemMessage(content='Please tell capital city of the country which user is asking', additional_kwargs={}, response_metadata={}), HumanMessage(content='US', additional_kwargs={}, response_metadata={})]

MessagePlaceholder

文字通り、会話履歴のように複数メッセージが入る Placeholder を使うことができます。

from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

prompt = ChatPromptTemplate.from_messages(
    [
        ("system", "You are a helpful assistant"),
        MessagesPlaceholder("chat_history", option=True),
        ("human", "{input}"),
    ])

prompt_value = prompt.invoke({
    "chat_history": [
        HumanMessage(content="Hello! My name is Jhon."),
        AIMessage("Hello Jhon, how can I help you?")
    ],
    "input": "What is my name again?"
})
print(prompt_value)

—>

messages=[SystemMessage(content='You are a helpful assistant', additional_kwargs={}, response_metadata={}), HumanMessage(content='Hello! My name is Jhon.', additional_kwargs={}, response_metadata={}), AIMessage(content='Hello Jhon, how can I help you?', additional_kwargs={}, response_metadata={}, tool_calls=[], invalid_tool_calls=[]), HumanMessage(content='What is my name again?', additional_kwargs={}, response_metadata={})]

Output parser

LLM からの出力において、特定の形式で出力させるコンポーネントです。

JSON など出力形式を指定するプロンプトの作成と、応答のテキストをオブジェクトに変換する機能を提供します。

回答から該当箇所を抽出して、辞書型や自作したクラスにマッピングすることが簡単にできます。

PydanticOutputParser

例として PydanticOutputParser を用いて LLM の出力を自動変換する例を見ていきましょう。

with_structured_output

from pydantic import BaseModel, Field
from langchain_core.output_parsers import PydanticOutputParser

class Recipe(BaseModel):
    ingredients: list[str] = Field(description="ingredients of the dish")
    steps: list[str] = Field(description="steps to make the dish")
    
output_parser = PydanticOutputParser(pydantic_object=Recipe)
format_instructions = output_parser.get_format_instructions()

print(format_instructions)
The output should be formatted as a JSON instance that conforms to the JSON schema below.

As an example, for the schema {"properties": {"foo": {"title": "Foo", "description": "a list of strings", "type": "array", "items": {"type": "string"}}}, "required": ["foo"]}
the object {"foo": ["bar", "baz"]} is a well-formatted instance of the schema. The object {"properties": {"foo": ["bar", "baz"]}} is not well-formatted.

Here is the output schema:
```
{"properties": {"ingredients": {"description": "ingredients of the dish", "items": {"type": "string"}, "title": "Ingredients", "type": "array"}, "steps": {"description": "steps to make the dish", "items": {"type": "string"}, "title": "Steps", "type": "array"}}, "required": ["ingredients", "steps"]}
```

JSON 形式での出力が求められてます。

これをプロンプトに埋め込むと

from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            "ユーザーが入力した料理のレシピを考えてください\n\n"
            "{format_instructions}",
        ),
        ("human", "{dish}")
    ]
)

prompt_with_format_instructions = prompt.partial(
    format_instructions = format_instructions
)
prompt_value = prompt_with_Format_instructions.invoke({"dish": "カレー"})
print("=== role: system ===")
print(prompt_value.messages[0].content)
print("=== role: user ===")
print(prompt_value.messages[1].content)

出力

=== role: system ===
ユーザーが入力した料理のレシピを考えてください

The output should be formatted as a JSON instance that conforms to the JSON schema below.

As an example, for the schema {"properties": {"foo": {"title": "Foo", "description": "a list of strings", "type": "array", "items": {"type": "string"}}}, "required": ["foo"]}
the object {"foo": ["bar", "baz"]} is a well-formatted instance of the schema. The object {"properties": {"foo": ["bar", "baz"]}} is not well-formatted.

Here is the output schema:
```
{"properties": {"ingredients": {"description": "ingredients of the dish", "items": {"type": "string"}, "title": "Ingredients", "type": "array"}, "steps": {"description": "steps to make the dish", "items": {"type": "string"}, "title": "Steps", "type": "array"}}, "required": ["ingredients", "steps"]}
```
=== role: user ===
カレー

モデルにリクエストを投げて回答をもらいます。

model = ChatAnthropic(
    model="claude-haiku-4-5-20251001",
    anthropic_api_key=os.getenv("CLAUDE_API"),
    temperature=0
)
ai_message = model.invoke(prompt_value)
print(ai_message.content)

出力

{
  "ingredients": [
    "豚肉 300g",
    "じゃがいも 2個",
    "にんじん 1本",
    "玉ねぎ 1個",
    "カレールー 100g",
    "水 400ml",
    "油 大さじ1",
    "塩 少々",
    "こしょう 少々"
  ],
  "steps": [
    "豚肉を一口大に切ります",
    "じゃがいも、にんじん、玉ねぎを乱切りにします",
    "鍋に油を熱し、豚肉を炒めます",
    "豚肉の色が変わったら、玉ねぎ、にんじん、じゃがいもを加えて炒めます",
    "野菜がしんなりしたら、水を加えます",
    "沸騰したら弱火にして、野菜が柔らかくなるまで煮込みます(約15分)",
    "火を止めてからカレールーを割り入れ、よく混ぜます",
    "再び火をつけ、弱火で5分程度煮込みます",
    "塩とこしょうで味を整えます",
    "器に盛り付けて完成です"
  ]
}

インスタンスに変換すると

recipe = output_parser.invoke(ai_message)
    print(type(recipe))
    print(recipe)
<class 'test_agent.main.<locals>.Recipe'>
ingredients=['豚肉 300g', 'じゃがいも 2個', 'にんじん 1本', '玉ねぎ 1個', 'カレールー 100g', '水 400ml', '油 大さじ1', '塩 少々', 'こしょう 少々']
steps=['豚肉を一口大に切ります', 'じゃがいも、にんじん、玉ねぎを乱切りにします', '鍋に油を熱し、豚肉を炒めます', '豚肉の色が変わったら、玉ねぎ、にんじん、じゃがいもを加えて炒めます', '野菜がしんなりしたら、水を加えます', '沸騰したら弱火にして、野菜が柔らかくなるまで煮込みます(約15分)', '火を止めてからカレールーを割り入れ、よく混ぜます', '再び火をつけ、弱火で5分程度煮込みます', '塩とこしょうで味を整えます', '器に盛り付けて完成です']

これで扱いやすいデータ形式になったかと思います。

注意:不完全な JSON が返された際にエラーを起こしてしまうことがございます。

構造化したデータが安定して返されるには、Chat Completions API の JSON モードを利用したり、Function Calling を応用させることが有効です。

LangChain Expression Language (LCEL)

LangChain での Chain 記述方法です、| でプロンプトや LLM を繋げて処理の Chain を実装します。

LangChain の標準的な書き方なので、大事なパートです。

Prompt と Model の Chain

単純に Prompt と Model を繋げた場合の例を見てみましょう。

from langchain_core.prompts import ChatPromptTemplate
from langchain_anthropic import ChatAnthropic

def main():
    prompt = ChatPromptTemplate.from_messages(
        [
            ("system", "ユーザーが入力した料理のレシピを考えてください"),
            ("human", "{dish}")
        ]
    )
    model = ChatAnthropic(
        model="claude-haiku-4-5-20251001",
        anthropic_api_key=os.getenv("CLAUDE_API"),
        temperature=0
    )

    chain = prompt | model

    ai_message = chain.invoke({"dish": "カレー"})
    print(ai_message.content)

出力

# カレーのレシピ

## 基本的なチキンカレー(4人分)

### 材料
- 鶏肉(もも肉):500g
- 玉ねぎ:2個
- にんじん:1本
- じゃがいも:2個
- カレールー:100g
- 水:600ml
- 油:大さじ2
- 塩・こしょう:少々

### 作り方

**1. 材料の準備**
- 鶏肉を一口大に切る
- 玉ねぎはくし切り、にんじんと じゃがいもは乱切りにする

**2. 炒める**
- 鍋に油を熱し、鶏肉を炒めて色が変わるまで加熱
- 玉ねぎを加えてしんなりするまで炒める

**3. 煮込む**
- 水を加えて沸騰させる
- にんじんとじゃがいもを加える
- 15分程度煮込む

**4. ルーを加える**
- カレールーを割って加える
- よく混ぜながら5分程度煮込む
- 塩・こしょうで味を整える

**5. 完成**
- ご飯にかけて召し上がれ!

### ポイント
- じゃがいもは煮崩れないよう注意
- ルーは焦げないようよく混ぜる
- 好みでヨーグルトやはちみつを加えても◎

何か質問があればお答えします!

StrOutPutParser

LLM からの出力をテキストに変換するのに使用されます。

from langchain_core.output_parsers import StrOutputParser
from langchain_core.messages import AIMessage

def main():
    output_parser = StrOutputParser()
    ai_message = AIMessage(content="Hello, I am your AI assistant")
    output = output_parser.invoke(ai_message)

    print(type(output))
    print(output)

出力

<class 'langchain_core.messages.base.TextAccessor'>
Hello, I am your AI assistant

これを Chain に組み込みます。

chain = prompt | model | StrOutputParser()
output = chain.invoke({"dish": "カレー"})
print(output)

ai_message だった部分が TextAccessor obejct になるので、.content はいりません。

(上記と大差ない回答が生成されます)

PydanticOutputParser を使う場合

from pydantic import BaseModel, Field
from langchain_core.output_parsers import PydanticOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_anthropic import ChatAnthropic

class Recipe(BaseModel):
    ingredients: list[str] = Field(description="ingredients of the dish")
    steps: list[str] = Field(description="steps to make the dish")

def main():
    output_parser = PydanticOutputParser(pydantic_object=Recipe)
    prompt = ChatPromptTemplate.from_messages(
        [
            ("system", "ユーザーが入力した料理のレシピを考えてください\n\n{format_instructions}"),
            ("human", "{dish}")
        ]
    )

    prompt_with_format_instructions = prompt.partial(
        format_instructions = output_parser.get_format_instructions()
    )

    model = ChatAnthropic(
        model="claude-haiku-4-5-20251001",
        anthropic_api_key=os.getenv("CLAUDE_API"),
        temperature=0
    )

    chain = prompt_with_format_instructions | model | output_parser

    output = chain.invoke({"dish": "カレー"})
    print(type(output))
    print(output)

ここで、PydanticOutputParser が JSON を強制してくれています。

ChatOpenAI の場合は更に出力形式を限定してくれます。

model = ChatAnthropic(
    model="gpt-4o-mini",
    anthropic_api_key=os.getenv("OPENAI_API"),
    temperature=0
).bind(
    response_format = {"type": "json_object"}
)

ChatAnthropic では json_object は対応しておりません。

出力

<class 'test_agent.Recipe'>
ingredients=['鶏肉 300g', '玉ねぎ 2個', 'じゃがいも 2個', '人参 1本', 'カレールー 100g', '水 400ml', '油 大さじ1', '塩 少々', 'こしょう 少々']
steps=['鶏肉を一口大に切ります', '玉ねぎ、じゃがいも、人参を一口大に切ります', '鍋に油を熱し、鶏肉を炒めます', '鶏肉の色が変わったら、玉ねぎを加えて炒めます', '玉ねぎが透明になったら、じゃがいもと人参を加えます', '全体を軽く炒めたら、水を加えます', '沸騰したら弱火にして、野菜が柔らかくなるまで15分程度煮込みます', 'カレールーを割り入れ、よく混ぜながら溶かします', 'とろみが出るまで5分程度煮込みます', '塩とこしょうで味を整えて完成です']

また、別の書き方として、

prompt | model.with_structured_output(Recipe)

も使えます。

LangSmith

LangChain の動作のトレースを簡単に収集することができるプラットフォーム。

サインアップし、左側ギアマーク Settings より API Keys を選択し、Personal Access Token で API Key を発行。

Google Colab にて以下のノートブックを作成し実行するだけで連携完了!(発行された API Key は LANGCHAIN_API_KEY に登録)

import os
from google.colab import userdata

os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_ENDPOINT"] = "https://apac.api.smith.langchain.com"
os.environ["LANGCHAIN_API_KEY"] = userdata.get("LANGCHAIN_API_KEY")
os.environ["LANGCHAIN_PROJECT"] = "agent-book"

Prompts にて testprompt というプロンプトを作成して公開

import os
from google.colab import userdata
from langsmith import Client

os.environ["LANGSMITH_TRACING"] = "true"
os.environ["LANGSMITH_ENDPOINT"] = "https://apac.api.smith.langchain.com"
os.environ["LANGSMITH_API_KEY"] = userdata.get("LANGSMITH_API_KEY")
os.environ["LANGSMITH_PROJECT"] = "agent-book"
os.environ["ANTHROPIC_API_KEY"] = userdata.get("ANTHROPIC_API_KEY")

client = Client()
prompt = client.pull_prompt(
    "pollytheparrot/testprompt",
    include_model=True,
    dangerously_pull_public_prompt=True,
    secrets={
        "ANTHROPIC_API_KEY": os.getenv("ANTHROPIC_API_KEY")
    }
)

result = prompt.invoke({
    "question": "Hi, my name is Justin"
})

print(result)

出力

content='Your name is Justin.' additional_kwargs={} response_metadata={'id': 'msg_011Cee2bYRxuoJVEdSzapsL3', 'container': None, 'model': 'claude-sonnet-5', 'stop_details': None, 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 26, 'output_tokens': 10, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet-5', 'model_provider': 'anthropic'} id='lc_run--01a06080-89ef-76d3-88ea-d68522a36814-0' tool_calls=[] invalid_tool_calls=[] usage_metadata={'input_tokens': 26, 'output_tokens': 10, 'total_tokens': 36, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}

あとがき

ちゃんと体系的に LangChain LangGraph をまとめてみたかったんですが、息子の出生などプライベートの変化が多く、なかなかまとまった時間が取れず、だいぶ新旧情報が混じっている記事になってしまったと反省しております。

本記事を書く際に理解を日本語化するのに参考させていただいた記事:

何かと普段でもお世話になっているまさぴょん氏に感謝!

次回からは LangChain の RAG においての便利なコンポーネントを紹介できればと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?