LangChain1

环境测试

#encoding=utf-8

"""
验证 MiniMax LLM 连通性
1. venv
2. pip install langchain langchain-openai langchain-community langgraph python-dotenv
3. Minimax API via OpenAI 兼容接口

python Src/Test/verify_llm.py
"""

import os
import time
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI

# 加载 .env(从项目根目录)
load_dotenv(dotenv_path=os.path.join(os.path.dirname(__file__), "..", "..", ".env"))

llm = ChatOpenAI(
    model="MiniMax-M2.7",
    base_url="https://api.minimax.chat/v1",
    streaming=True,          # ✅ 改动1: 开启流式输出
)

start = time.time()
first_token_time = None

# ✅ 改动2: invoke() → stream(),返回的是一个迭代器,每次yield一小块内容
for chunk in llm.stream("你好,请用一句话介绍你自己。"):
    if first_token_time is None:
        first_token_time = time.time() - start
    print(chunk.content, end="", flush=True)  # ✅ 改动3: 逐块打印,flush=True 立即刷新到屏幕

elapsed = time.time() - start
print()  # 换行
print(f"\n首token延迟: {first_token_time:.2f}s | 总耗时: {elapsed:.2f}s")

准备一个 AI 的 API,通过脚本调用大模型,

这里面涉及读取API key,用代码问问题,调用invoke获取问题输出,如果觉得慢可以用流式输出。

chat model

import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage

# ── 加载环境变量 ──
load_dotenv(dotenv_path=os.path.join(os.path.dirname(__file__), "..", "..", ".env"))

# ══════════════════════════════════════════════════════════════
# 第一步:创建 Chat Model 实例
# ══════════════════════════════════════════════════════════════
# ChatOpenAI 兼容所有 OpenAI 格式的 API(包括 MiniMax、DeepSeek 等)
# 它读取环境变量 OPENAI_API_KEY 作为密钥

llm = ChatOpenAI(
    model="MiniMax-M2.7",
    base_url="https://api.minimax.chat/v1",
    temperature=0.7,    # 控制随机性:0=确定性输出,1=更有创意,默认0.7
    # max_tokens=200,   # 限制最大输出 token 数(不设则由模型决定)
)

# ══════════════════════════════════════════════════════════════
# 第二步:理解「消息」—— Chat Model 的输入格式
# ══════════════════════════════════════════════════════════════
# 与大模型对话,实际发送的是一个消息列表,每条消息有「角色」:
#
#   SystemMessage  → 系统指令,定义模型的行为/人设(模型会始终遵守)
#   HumanMessage   → 用户说的话
#   AIMessage      → 模型之前的回复(用于多轮对话时提供历史上下文)
#
# 这就是 OpenAI Chat Completion API 的标准格式,LangChain 只是封装了它。

messages = [
    SystemMessage(content="你是一个简洁的助手,回答控制在50字以内。"),
    HumanMessage(content="什么是 LangChain?"),
]

print("=" * 60)
print("【演示1】基本调用 - invoke()")
print("=" * 60)

# invoke() = 发送消息,等待完整回复
response = llm.invoke(messages)

# 返回值是一个 AIMessage 对象,不是字符串
print(f"返回类型: {type(response).__name__}")   # AIMessage
print(f"内容: {response.content}")
print(f"模型: {response.response_metadata.get('model_name', 'N/A')}")
# ══════════════════════════════════════════════════════════════
# 第三步:快捷用法 —— 直接传字符串
# ══════════════════════════════════════════════════════════════
# LangChain 允许直接传字符串,它会自动包装成 [HumanMessage(content=...)]
# 方便,但没法加 SystemMessage

print("\n" + "=" * 60)
print("【演示2】直接传字符串(自动包装为 HumanMessage)")
print("=" * 60)

response2 = llm.invoke("用一句话解释 Python 的 GIL")
print(response2.content)
# ══════════════════════════════════════════════════════════════
# 第四步:流式输出 - stream()
# ══════════════════════════════════════════════════════════════
# invoke() 要等全部生成完才返回(慢)
# stream() 边生成边返回,每次拿到一小块(chunk)

print("\n" + "=" * 60)
print("【演示3】流式输出 - stream()")
print("=" * 60)

for chunk in llm.stream("用3个要点介绍 Python 的优势"):
    print(chunk.content, end="", flush=True)
print()  # 换行
# ══════════════════════════════════════════════════════════════
# 第五步:批量调用 - batch()
# ══════════════════════════════════════════════════════════════
# 一次发送多个请求,并发执行,比循环 invoke() 更快

print("\n" + "=" * 60)
print("【演示4】批量调用 - batch()")
print("=" * 60)

questions = [
    "Python 适合做什么?回答20字以内",
    "Java 适合做什么?回答20字以内",
    "Rust 适合做什么?回答20字以内",
]
responses = llm.batch(questions)

for q, r in zip(questions, responses):
    print(f"Q: {q}")
    print(f"A: {r.content}\n")
# ══════════════════════════════════════════════════════════════
# 第六步:多轮对话 —— 手动管理消息历史
# ══════════════════════════════════════════════════════════════
# 大模型本身是「无状态」的 —— 每次调用都是独立的,它不记得之前说过什么。
# 要实现多轮对话,必须把之前的对话历史一起发送给模型。

print("\n" + "=" * 60)
print("【演示5】多轮对话 - 手动传入历史消息")
print("=" * 60)

conversation = [
    SystemMessage(content="你是一个耐心的编程导师。"),
    HumanMessage(content="什么是变量?"),
]

# 第一轮
reply1 = llm.invoke(conversation)
print(f"第1轮 → {reply1.content}\n")

# 把模型的回复加入历史,再追问
conversation.append(reply1)                          # AIMessage 加入历史
conversation.append(HumanMessage(content="举个例子")) # 新的用户问题

# 第二轮 —— 模型能看到之前的完整对话
reply2 = llm.invoke(conversation)
print(f"第2轮 → {reply2.content}")

Prompt Template

1.2 Prompt Template — 提示词模板
================================
本脚本演示为什么需要 Prompt Template,以及如何用它组装复杂的提示词。

核心知识点:

  1. 为什么不直接拼字符串?—— 模板可复用、可变量替换、自动处理消息角色
  2. ChatPromptTemplate = 消息模板列表,每条模板有角色 + 变量占位符
  3. MessagesPlaceholder = 在模板中「留一个口子」,后续插入动态消息列表(多轮对话的关键)
  4. Prompt Template 也是 Runnable,可以 invoke / stream / 用 | 接入管道

运行:python Src/01_basics/prompt_template.py

# ══════════════════════════════════════════════════════════════
# 第一步:为什么不直接拼字符串?
# ══════════════════════════════════════════════════════════════
# 1.1 里我们手动写消息列表:
#   messages = [SystemMessage(content="你是..."), HumanMessage(content="...")]
#
# 问题:如果要做一个翻译助手,语言和文本都是动态的,每次手动拼很繁琐。
# Prompt Template 就是解决这个问题的——定义一次模板,多次填变量复用。

print("=" * 60)
print("【演示1】基本模板 - 变量替换")
print("=" * 60)

# 定义模板:{language} 和 {text} 是变量占位符
# "system" / "human" 是角色,对应 SystemMessage / HumanMessage
prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个专业翻译,请将用户的文本翻译成{language}。只输出翻译结果,不要解释。"),
    ("human", "{text}"),
])

# 查看模板里有哪些变量
print(f"模板变量: {prompt.input_variables}")  # ['language', 'text']

# invoke() 填入变量 → 得到完整的消息列表
messages = prompt.invoke({"language": "英语", "text": "今天天气真好"})
print(f"生成的消息:\n{messages.to_messages()}\n")

# 把生成的消息发给模型
response = llm.invoke(messages)
print(f"翻译结果: {response.content}")
# ══════════════════════════════════════════════════════════════
# 第二步:模板复用 —— 同一模板,不同参数
# ══════════════════════════════════════════════════════════════
# 这就是模板的价值:定义一次,用无数次

print("\n" + "=" * 60)
print("【演示2】模板复用 - 同一模板翻译成不同语言")
print("=" * 60)

for lang in ["日语", "法语", "韩语"]:
    messages = prompt.invoke({"language": lang, "text": "我正在学习 LangChain"})
    response = llm.invoke(messages)
    print(f"  {lang}: {response.content}")

两次invoke有点丑,实际上 管道 更像一种语法糖

# ══════════════════════════════════════════════════════════════
# 第三步:Prompt Template 也是 Runnable → 可以用管道
# ══════════════════════════════════════════════════════════════
# 这是为 1.4 LCEL Chain 做铺垫

print("\n" + "=" * 60)
print("【演示3】管道语法预览: prompt | model")
print("=" * 60)

# prompt.invoke(变量) 输出消息列表 → 自动传给 llm.invoke()
chain = prompt | llm

# 现在直接传变量字典就行,不用手动分两步
result = chain.invoke({"language": "英语", "text": "机器学习是人工智能的子领域"})
print(f"管道输出: {result.content}")

聊天历史如何插入:

# ══════════════════════════════════════════════════════════════
# 第四步:MessagesPlaceholder —— 插入动态消息列表
# ══════════════════════════════════════════════════════════════
# 场景:做一个带上下文记忆的对话机器人
# 系统提示词是固定的,但历史对话是动态的(每轮都在增长)
# MessagesPlaceholder 就是在模板里「留一个口子」,运行时塞入消息列表

print("\n" + "=" * 60)
print("【演示4】MessagesPlaceholder - 插入聊天历史")
print("=" * 60)

prompt_with_history = ChatPromptTemplate.from_messages([
    ("system", "你是一个有耐心的编程导师,回答简洁。"),
    MessagesPlaceholder(variable_name="chat_history"),  # ← 留口子:聊天历史插这里
    ("human", "{question}"),                             # ← 当前问题
])

# 模拟两轮对话后的历史记录
fake_history = [
    HumanMessage(content="Python 里怎么定义函数?"),
    AIMessage(content="用 def 关键字,比如 def my_func():"),
    HumanMessage(content="函数参数怎么写?"),
    AIMessage(content="括号里写参数名,如 def add(a, b): return a + b"),
]

# 填入变量:chat_history 是消息列表,question 是字符串
messages = prompt_with_history.invoke({
    "chat_history": fake_history,
    "question": "那默认参数呢?",  # 第三轮追问
})

# 看看最终组装出的消息列表(系统 + 4条历史 + 新问题 = 6条消息)
print(f"总消息数: {len(messages.to_messages())}")
for msg in messages.to_messages():
    role = type(msg).__name__.replace("Message", "")
    print(f"  [{role}] {msg.content[:50]}...")

# 发给模型
response = llm.invoke(messages)
print(f"\n模型回答: {response.content}")
# ══════════════════════════════════════════════════════════════
# 第五步:from_template —— 更简单的单条消息写法
# ══════════════════════════════════════════════════════════════

print("\n" + "=" * 60)
print("【演示5】from_template - 快速创建简单模板")
print("=" * 60)

# 如果只需要一条 HumanMessage,可以用更简洁的写法
simple_prompt = ChatPromptTemplate.from_template(
    "用{style}的风格,一句话概括:{topic}"
)
print(f"模板变量: {simple_prompt.input_variables}")

chain = simple_prompt | llm
result = chain.invoke({"style": "程序员", "topic": "什么是递归"})
print(f"输出: {result.content}")

Output Parser

1.3 Output Parser — 输出解析器
==============================
本脚本演示如何把模型返回的「自然语言文本」转换成程序可用的结构化数据。

核心问题:
模型返回的永远是字符串(自然语言),但程序需要 JSON、对象、列表等结构化数据。
Output Parser 就是这个「翻译层」。

三种 Parser 对比:
StrOutputParser → 最简单,AIMessage → 纯字符串
JsonOutputParser → 让模型输出 JSON,解析成 dict
PydanticOutputParser → 让模型输出符合指定 Schema 的 JSON,解析成 Python 对象

运行:python Src/01_basics/output_parser.py

问题:

# ══════════════════════════════════════════════════════════════
# 第一步:没有 Parser 时的问题
# ══════════════════════════════════════════════════════════════

print("=" * 60)
print("【演示0】没有 Parser —— 模型返回的是 AIMessage 对象")
print("=" * 60)

response = llm.invoke("1+1等于几?只回答数字")
print(f"类型: {type(response)}")          # AIMessage
print(f"内容: {response.content}")         # 字符串
print(f"直接做数学? {type(response.content)} 无法直接 +1")
# 你需要手动 .content 取文本,再自己解析——这就是 Parser 要解决的问题
# ══════════════════════════════════════════════════════════════
# 第二步:StrOutputParser —— AIMessage → 纯字符串
# ══════════════════════════════════════════════════════════════
# 最简单的 Parser:把 AIMessage 对象变成纯字符串
# 看起来简单,但在管道里非常有用(后续组件通常需要字符串而不是 AIMessage)

print("\n" + "=" * 60)
print("【演示1】StrOutputParser - AIMessage → 字符串")
print("=" * 60)

str_parser = StrOutputParser()

# 单独使用
parsed = str_parser.invoke(response)  # 传入上面的 AIMessage
print(f"类型: {type(parsed)}")   # str
print(f"内容: {parsed}")

# 更常见:在管道中使用
chain = llm | str_parser
result = chain.invoke("Python 的创始人是谁?一个名字即可")
print(f"管道输出: {result}")         # 直接就是字符串,不用 .content
print(f"类型: {type(result)}")       # str

构建是忽悠大模型输出dict

# ══════════════════════════════════════════════════════════════
# 第三步:JsonOutputParser —— 让模型输出 JSON
# ══════════════════════════════════════════════════════════════
# 场景:你需要模型返回结构化数据,比如「提取一本书的信息」

print("\n" + "=" * 60)
print("【演示2】JsonOutputParser - 模型输出 → Python dict")
print("=" * 60)

json_parser = JsonOutputParser()

# 关键:parser.get_format_instructions() 生成一段提示词,告诉模型要输出 JSON
print(f"格式指令:\n{json_parser.get_format_instructions()}\n")

prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个信息提取助手。{format_instructions}"),
    ("human", "提取这本书的信息:《三体》是刘慈欣写的科幻小说,2008年出版。请返回 title、author、year、genre 字段。"),
])

# 把格式指令填入模板
chain = prompt | llm | json_parser

result = chain.invoke({"format_instructions": json_parser.get_format_instructions()})
print(f"类型: {type(result)}")       # dict
print(f"结果: {result}")
print(f"取值: 书名={result.get('title')}, 作者={result.get('author')}")

这里还涉及了thinking 模型的输出内容影响了 json解析

# ── 处理推理模型的 <think> 标签 ──
# MiniMax-M2.7 是推理模型,输出会带 <think>思考过程</think> 前缀
# Parser 无法解析这些内容,需要先去掉
# RunnableLambda 可以把任意函数包装成 Runnable,插到管道里
def strip_think_tags(ai_message):
    """去掉模型输出中的 <think>...</think> 思考内容"""
    text = ai_message.content
    cleaned = re.sub(r"<think>.*?</think>\s*", "", text, flags=re.DOTALL)
    ai_message.content = cleaned
    return ai_message

strip_think = RunnableLambda(strip_think_tags)

用的时候多一个

chain = prompt | llm | strip_think | pydantic_parser

所以最后这个实例就是把大模型回答转为Python对象


# ══════════════════════════════════════════════════════════════
# 第四步:PydanticOutputParser —— 模型输出 → Python 对象
# ══════════════════════════════════════════════════════════════
# JsonOutputParser 返回 dict,但 dict 没有类型校验、没有自动补全
# PydanticOutputParser 更进一步:定义一个 Class,模型输出必须符合这个 Schema

print("\n" + "=" * 60)
print("【演示3】PydanticOutputParser - 模型输出 → Pydantic 对象")
print("=" * 60)

# 第一步:定义数据模型(Schema)
# Pydantic 的 Field(description=...) 会被自动转成提示词,告诉模型每个字段是什么意思
class MovieInfo(BaseModel):
    title: str = Field(description="电影名称")
    director: str = Field(description="导演姓名")
    year: int = Field(description="上映年份")
    genre: str = Field(description="电影类型,如:科幻、喜剧、动作")
    rating: float = Field(description="评分,0-10分")

# 第二步:创建 Parser
pydantic_parser = PydanticOutputParser(pydantic_object=MovieInfo)

# 看看生成的格式指令——它会包含完整的 JSON Schema
print(f"格式指令:\n{pydantic_parser.get_format_instructions()}\n")

# 第三步:组装管道
prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个影视信息提取助手。{format_instructions}"),
    ("human", "提取这部电影的信息:{movie_description}"),
])

# 同样需要 strip_think 去掉推理模型的思考内容
chain = prompt | llm | strip_think | pydantic_parser

movie = chain.invoke({
    "format_instructions": pydantic_parser.get_format_instructions(),
    "movie_description": "《星际穿越》是克里斯托弗·诺兰执导的科幻电影,2014年上映,豆瓣评分9.4。",
})

# 返回的是 MovieInfo 对象,有类型校验和属性访问
print(f"类型: {type(movie)}")           # MovieInfo
print(f"电影: {movie.title}")
print(f"导演: {movie.director}")
print(f"年份: {movie.year} (类型: {type(movie.year).__name__})")  # int,不是字符串!
print(f"类型: {movie.genre}")
print(f"评分: {movie.rating}")

Tool_calling

#encoding=utf-8

"""
2.2 Tool Calling — 让模型学会「使用工具」
=========================================
本脚本演示 LangChain 中 Tool Calling 的完整流程。

核心知识点:
  1. 用 @tool 装饰器定义工具(名称 + 描述 + 参数 → 自动生成 JSON Schema)
  2. model.bind_tools() 把工具"告诉"模型(模型不会自动知道有哪些工具)
  3. 模型返回的 AIMessage.tool_calls 是结构化的调用请求(不是执行结果!)
  4. 你需要自己拿到 tool_calls,执行对应函数,再把结果喂回模型
  5. ToolMessage 用来把工具执行结果返回给模型,完成"闭环"

关键理解:
  - Tool Calling ≠ 模型执行函数。模型只是"说"它想调用什么,真正执行的是你的代码
  - 这是 Agent 的核心机制:模型决策 → 工具执行 → 结果反馈 → 模型再决策

运行:python Src/02_tools/tool_calling.py
"""

import os
import json
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, AIMessage, ToolMessage

# ── 加载环境变量 ──
load_dotenv(dotenv_path=os.path.join(os.path.dirname(__file__), "..", "..", ".env"))

llm = ChatOpenAI(
    model="MiniMax-M2.7",
    base_url="https://api.minimax.chat/v1",
    temperature=0,
)

# ══════════════════════════════════════════════════════════════
# 第一步:定义工具 — 用 @tool 装饰器
# ══════════════════════════════════════════════════════════════
# @tool 做了三件事:
#   1. 从函数名 → 工具名
#   2. 从 docstring → 工具描述(模型靠这个理解工具用途)
#   3. 从类型注解 → 参数 JSON Schema(模型靠这个知道传什么参数)
#
# 所以:描述写得好不好,直接决定模型会不会正确调用你的工具!

@tool
def get_weather(city: str) -> str:
    """查询指定城市的当前天气。当用户问天气相关问题时使用。"""
    # 这里模拟返回,实际项目中会调用真实天气 API
    weather_data = {
        "北京": "晴天,25°C",
        "上海": "多云,22°C",
        "深圳": "小雨,28°C",
    }
    return weather_data.get(city, f"抱歉,暂无{city}的天气数据")

@tool
def calculator(expression: str) -> str:
    """计算数学表达式。当用户需要数学计算时使用。输入应为合法的数学表达式,如 '2 + 3 * 4'。"""
    try:
        # 注意:生产环境不要用 eval,这里仅做演示
        result = eval(expression, {"__builtins__": {}})
        return str(result)
    except Exception as e:
        return f"计算错误: {e}"

tools = [get_weather, calculator]

print("=" * 60)
print("【演示1】查看工具的 Schema(模型看到的就是这个)")
print("=" * 60)

for t in tools:
    print(f"\n工具名: {t.name}")
    print(f"描述: {t.description}")
    print(f"参数 Schema: {json.dumps(t.args_schema.model_json_schema(), ensure_ascii=False, indent=2)}")

# ══════════════════════════════════════════════════════════════
# 第二步:bind_tools() — 把工具绑定到模型
# ══════════════════════════════════════════════════════════════
# bind_tools() 不是"安装"工具,而是在每次请求中附带工具的 Schema
# 这样模型就知道有哪些工具可用,以及每个工具需要什么参数
#
# 数据流:
#   tools 的 Schema → 附加到 API 请求的 tools 字段 → 模型看到后决定是否调用

llm_with_tools = llm.bind_tools(tools)

print("\n" + "=" * 60)
print("【演示2】模型决定调用工具(不是直接回答)")
print("=" * 60)

# 问一个需要工具才能回答的问题
response = llm_with_tools.invoke("北京今天天气怎么样?")

# 关键!返回的是 AIMessage,但 content 可能是空的
# 因为模型选择了"调用工具"而不是"直接回答"
print(f"content: '{response.content}'")
print(f"tool_calls: {response.tool_calls}")

# tool_calls 是一个列表,每个元素是一个字典:
#   {
#     "name": "get_weather",     ← 要调用哪个工具
#     "args": {"city": "北京"},  ← 传什么参数(模型自动从问题中提取)
#     "id": "call_xxx"           ← 调用 ID,后面 ToolMessage 要用
#   }

# ══════════════════════════════════════════════════════════════
# 第三步:执行工具 — 你来跑,不是模型跑
# ══════════════════════════════════════════════════════════════
# 拿到 tool_calls 后,需要你自己找到对应的函数并执行
# 这就是"Tool Calling"的核心:模型负责决策,你负责执行

print("\n" + "=" * 60)
print("【演示3】手动执行工具并获取结果")
print("=" * 60)

# 建一个 name → function 的映射表
tool_map = {t.name: t for t in tools}

tool_messages = []
for tc in response.tool_calls:
    tool_name = tc["name"]
    tool_args = tc["args"]
    tool_id = tc["id"]

    print(f"模型想调用: {tool_name}({tool_args})")

    # 执行工具(.invoke() 就是调用这个函数)
    result = tool_map[tool_name].invoke(tool_args)
    print(f"执行结果: {result}")

    # 把结果包装成 ToolMessage
    # tool_call_id 必须和 AIMessage 中的 id 对应,模型靠这个配对
    tool_messages.append(
        ToolMessage(content=result, tool_call_id=tool_id)
    )

# ══════════════════════════════════════════════════════════════
# 第四步:把工具结果喂回模型 — 完成闭环
# ══════════════════════════════════════════════════════════════
# 完整的消息流:
#   HumanMessage("北京天气?")
#     → AIMessage(tool_calls=[...])       ← 模型说"我要查天气"
#     → ToolMessage("晴天,25°C")          ← 你执行后告诉模型结果
#     → AIMessage("北京今天晴天,25度")     ← 模型用结果组织最终回答
#
# 这就是一个完整的 Tool Calling 循环

print("\n" + "=" * 60)
print("【演示4】完整闭环 — 模型根据工具结果生成最终回答")
print("=" * 60)

# 构造完整的消息历史
messages = [
    HumanMessage(content="北京今天天气怎么样?"),
    response,           # AIMessage(含 tool_calls)
    *tool_messages,     # ToolMessage(工具执行结果)
]

# 再次调用模型,这次它会根据工具结果生成自然语言回答
final_response = llm_with_tools.invoke(messages)
print(f"最终回答: {final_response.content}")

# ══════════════════════════════════════════════════════════════
# 第五步:模型不一定调用工具 — 看问题而定
# ══════════════════════════════════════════════════════════════
# 绑定了工具不代表每次都会调用
# 如果问题不需要工具,模型会直接回答

print("\n" + "=" * 60)
print("【演示5】不需要工具时,模型直接回答")
print("=" * 60)

response_no_tool = llm_with_tools.invoke("你好,介绍一下你自己")
print(f"content: '{response_no_tool.content}'")
print(f"tool_calls: {response_no_tool.tool_calls}")  # 应该是空列表

# ══════════════════════════════════════════════════════════════
# 第六步:一次调用多个工具 — parallel tool calling
# ══════════════════════════════════════════════════════════════
# 有些模型支持一次返回多个 tool_calls(并行调用)
# 你需要遍历所有 tool_calls,逐个执行,再把所有 ToolMessage 一起喂回

print("\n" + "=" * 60)
print("【演示6】多工具调用 — 一个问题触发多个工具")
print("=" * 60)

multi_response = llm_with_tools.invoke("北京和上海的天气分别是什么?另外帮我算一下 123 * 456")
print(f"tool_calls 数量: {len(multi_response.tool_calls)}")

multi_tool_messages = []
for tc in multi_response.tool_calls:
    result = tool_map[tc["name"]].invoke(tc["args"])
    print(f"  {tc['name']}({tc['args']}) → {result}")
    multi_tool_messages.append(
        ToolMessage(content=result, tool_call_id=tc["id"])
    )

# 把所有结果喂回模型
if multi_tool_messages:
    full_messages = [
        HumanMessage(content="北京和上海的天气分别是什么?另外帮我算一下 123 * 456"),
        multi_response,
        *multi_tool_messages,
    ]
    final = llm_with_tools.invoke(full_messages)
    print(f"\n最终回答: {final.content}")
上一篇