LangChain の勉強のために、スマホの機種や料金プラン選びを手伝うエージェントを作りました。
利用条件を聞き取って、Web で調べて、総額を計算して比べてくれる CLI のチャットです。コードは全部載せています。
環境
- Python 3.13 / uv
-
langchain1.4.4(create_agentを使用) -
langchain-google-genai4.4.1(モデル:gemini-2.5-flash) -
langchain-tavily0.2.18(Web 検索) python-dotenv
.
├── agent.py # エージェントを組み立てる
├── main.py # CLI のチャットループ
├── prompts.py # システムプロンプト
├── tools/
│ ├── __init__.py # ツールをまとめる
│ ├── search.py # web_search(Tavily)
│ ├── calc.py # calc_total_cost
│ └── date.py # get_today
└── .env # APIキー(git 管理外)
APIキーは .env に置いた
Gemini のキーは Google AI Studio、Tavily のキーは Tavily のサイトで発行できます。
# このファイルを .env にコピーして値を入れる
GOOGLE_API_KEY=
TAVILY_API_KEY=
# 省略時は gemini-2.5-flash
MODEL=
.env は .gitignore に入れて、リポジトリには .env.example(値は空)だけを置いています。
ツールを3つ用意した
エージェントに持たせたのは次の3つです。
| ツール | 役割 |
|---|---|
web_search |
料金プランや端末の情報を調べる |
calc_total_cost |
端末代と月額から総額を計算する |
get_today |
今日の日付を返す(キャンペーン期限の判断用) |
総額の計算はツールに任せた
LLM に「24か月でいくら?」を暗算させると数字がずれることがあるので、計算は関数にしました。
from langchain.tools import tool
@tool
def calc_total_cost(
device_price: int,
monthly_fee: int,
months: int = 24,
discount: int = 0,
) -> dict:
"""端末代と月額料金から、指定した月数の総額を計算する。
Args:
device_price: 端末代(円)。今の端末を使うなら0。
monthly_fee: 月額料金(円)。
months: 利用月数。
discount: 割引やポイント還元の合計(円)。
"""
total = device_price + monthly_fee * months - discount
return {
"total": total,
"monthly_average": round(total / months),
"formula": f"{device_price} + {monthly_fee} x {months} - {discount} = {total}",
}
@tool を付けると、docstring と型ヒントがそのまま LLM へのツール説明になります。Args: に「今の端末を使うなら0」のように書いておくと、引数の埋め方を LLM が判断しやすくなります。
戻り値に formula(計算式)を入れたのは、回答の中で計算の根拠を見せられるようにするためです。
日付もツールにした
LLM は今日の日付を知らないので、キャンペーンが終わっているかどうかを判断できません。そこで日付を返すだけのツールを作りました。
from datetime import date
from langchain.tools import tool
@tool
def get_today() -> str:
"""今日の日付を YYYY-MM-DD 形式で返す。キャンペーン期限や情報の新しさの判断に使う。"""
return date.today().isoformat()
検索は Tavily をそのまま使った
langchain-tavily の TavilySearch はそのままツールとして渡せます。プロンプトから呼び名をそろえたかったので、name="web_search" にしました。
from langchain_tavily import TavilySearch
def make_web_search() -> TavilySearch:
"""環境変数 TAVILY_API_KEY を使う。"""
return TavilySearch(name="web_search", max_results=5)
TavilySearch は作った時点で環境変数 TAVILY_API_KEY を読みに行きます。モジュールの先頭で作ると .env を読み込む前に動いてしまうので、関数にして、エージェントを組み立てるときに呼ぶようにしています。
from tools.calc import calc_total_cost
from tools.date import get_today
from tools.search import make_web_search
def build_tools() -> list:
return [make_web_search(), calc_total_cost, get_today]
システムプロンプトで進め方を決めた
相手はスマホの料金に詳しくない人、という想定にしました。最初から全部聞くと答えにくいので、1〜2問ずつ選択肢付きで聞くように書いています。
SYSTEM_PROMPT = """\
あなたは携帯電話の端末と料金プラン選びを手伝うアシスタントです。
相手はスマホの料金や用語に詳しくない人だと想定してください。
## 進め方
1. まず利用条件を聞き取る。一度に全部聞かず、1〜2問ずつ、選択肢を示して選んでもらう。
- 月のデータ量(例: ほぼWi-Fi / 3GB未満 / 20GB前後 / 無制限)
- 通話(ほとんどしない / 5分以内が多い / 長電話をよくする)
- 希望機種(iPhone / Android / こだわらない / 今の端末を使う)
- 予算(月額・端末代)
2. 条件がそろったら web_search で料金プランや端末の情報を調べる。
3. 総額の比較は必ず calc_total_cost を使う。自分で暗算しない。
4. キャンペーンの期限や情報の新しさは get_today で今日の日付を確認して判断する。
## 回答ルール
- 料金・キャンペーンには必ず出典URLを付ける。
- 「料金や条件は変わる可能性があるため、契約前に各社のサイトで確認してください」と添える。
- 専門用語は短く言い換える(例: MNP = 番号そのまま乗り換え)。
"""
ツールの名前(web_search、calc_total_cost、get_today)をプロンプトの中で直接書いて、いつどれを使うかを指定しています。
エージェントを組み立てた
LangChain 1.x の create_agent に、LLM・ツール・システムプロンプトを渡すだけです。temperature=0 にして、毎回の答えがぶれにくいようにしました。
import os
from dotenv import load_dotenv
from langchain.agents import create_agent
from langchain_google_genai import ChatGoogleGenerativeAI
from prompts import SYSTEM_PROMPT
from tools import build_tools
load_dotenv()
DEFAULT_MODEL = "gemini-2.5-flash"
def build_agent():
llm = ChatGoogleGenerativeAI(
model=os.getenv("MODEL") or DEFAULT_MODEL,
temperature=0,
)
return create_agent(llm, tools=build_tools(), system_prompt=SYSTEM_PROMPT)
ChatGoogleGenerativeAI は環境変数 GOOGLE_API_KEY を使います。モデルは .env の MODEL で切り替えられるようにしました。
CLI のチャットループ
会話の履歴は messages に全部持っておき、毎回まるごとエージェントに渡しています。こうすると、前に聞き取った条件を覚えたまま話を続けられます。
from agent import build_agent
def main():
agent = build_agent()
messages = []
print("携帯相談Bot です。終了は exit")
while True:
user = input("\nあなた> ").strip()
if user in ("exit", "quit"):
break
if not user:
continue
messages.append({"role": "user", "content": user})
result = agent.invoke({"messages": messages})
new_messages = result["messages"][len(messages):]
messages = result["messages"]
for m in new_messages:
for call in getattr(m, "tool_calls", None) or []:
print(f" [tool] {call['name']}({call['args']})")
print(f"\nBot> {messages[-1].text}")
if __name__ == "__main__":
main()
agent.invoke() の戻り値には、ツールの呼び出しと結果も含めた会話全体が入っています。今回増えた分(new_messages)から tool_calls を取り出して表示しているので、どのツールがどんな引数で呼ばれたかが見えます。
起動はこれだけです。
uv sync
cp .env.example .env # キーを入れる
uv run python main.py
おわりに
ツールを「関数+docstring」で書くだけで LLM が使ってくれるので、思っていたより少ないコードで形になりました。
calc_total_cost に months=0 が渡ると 0 除算で落ちるなど、入力のチェックはまだ入れていません。自分で使う分にはこれで様子を見ています。