环境测试
#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,以及如何用它组装复杂的提示词。
核心知识点:
- 为什么不直接拼字符串?—— 模板可复用、可变量替换、自动处理消息角色
- ChatPromptTemplate = 消息模板列表,每条模板有角色 + 变量占位符
- MessagesPlaceholder = 在模板中「留一个口子」,后续插入动态消息列表(多轮对话的关键)
- 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}")