本記事より 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 においての便利なコンポーネントを紹介できればと思います。
