二、聊天模型核心能力

1. 定义聊天模型

大语言模型 (LLM) 在各种与语言相关的任务(例如文本生成、翻译、摘要、问答等)中表现出色。 现代 LLM 通常通过聊天模型接口访问,该接口将消息列表作为输入,并返回消息作为输出,而不是使 用纯文本。

LLM 与 LangChain 中 聊天模型 的关系:

• 在 LangChain 的官方文档中,认为 LLM 大多数是纯文本补全模型。这些纯文本模型封装的 API 接 受一个字符串提示作为输入,并输出一个字符串补全结果(实际上 LLM 还包括多模态输入)。 OpenAI 的 GPT-5 就是作为 LLM 来实现的。

• LangChain 中的 聊天模型 通常由 LLM 提供支持,但经过专门调整以用于对话。关键在于,它们 不是接受单个字符串作为输入,而是接受聊天消息列表,并返回一条 AI 消息作为输出。

1.1 通过 API 定义聊天模型

1.1.1 方式1:ChatOpenAI

class langchain_openai.chat_models.base.ChatOpenAI 是 LangChain 为 OpenAI 的 聊天模型(如 gpt-5 , gpt-5-mini )提供的具体实现类。

其继承了 class langchain_openai.chat_models.base.BaseChatOpenAI ,且 BaseChatOpenAI 实现了标准的 Runnable 接口。

ChatOpenAI 常用初始化参数说明

参数名

参数描述

备注

model

要使用的 OpenAI 模型的名称(如 gpt-4, gpt-3.5-turbo等)

核心参数,必须指定

temperature

采样温度,控制回答的随机性。值越高,回答越多样化、有创意;值越低,回答越稳定、保守。

常用范围是 0.0 ~ 1.0

max_tokens

要生成的最大令牌数(即回复的最大长度)

用于控制生成长度

timeout

请求 API 的超时时间

网络控制参数

max_retries

API 调用失败后的最大重试次数

网络控制参数

openai_api_key / api_key

OpenAI API 密钥。用于身份验证。

如未传入,默认从环境变量 OPENAI_API_KEY中读取

base_url

API 请求的基本 URL。

可用于配置代理或使用兼容 OpenAI 的第三方服务

organization

OpenAI 组织 ID。

如未传入,默认从环境变量 OPENAI_ORG_ID中读取

代码里我们使用deepseek的,正好v4版本上线了

DeepSeek 常用参数表

参数名 作用 常用写法 / 推荐值
model 指定模型 deepseek-chatdeepseek-reasonerdeepseek-v4-flashdeepseek-v4-pro
temperature 控制随机性,越低越稳定,越高越发散 编程/数学:0;数据分析:1.0;聊天/翻译:1.3;创作:1.5。DeepSeek 默认是 1.0
max_tokens 限制最大输出 token 数 例如 10242048,不写就是让模型默认控制
timeout 请求超时时间 例如 30,单位通常是秒
max_retries 请求失败后的最大重试次数 例如 23
top_p 另一种控制随机性的采样参数 默认 1,通常不要和 temperature 同时乱调
presence_penalty 鼓励模型聊新内容 范围 -22,默认 0
frequency_penalty 降低重复表达 范围 -22,默认 0
stream 是否流式输出 True / False
stop 遇到指定字符串停止生成 例如 ["\n\n"]
response_format 控制输出格式 JSON 输出时用 {"type": "json_object"}
tools 函数调用 / 工具调用 用于让模型调用函数,DeepSeek 当前最多支持 128 个 functions。
api_key / DEEPSEEK_API_KEY API 密钥 推荐配置环境变量 DEEPSEEK_API_KEY

from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import Runnable, RunnableSequence
from langchain_deepseek import ChatDeepSeek
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_openai import ChatOpenAI
#1、定义模型
#api key默认从系统环境变量中读取()里的第二个参数也可以自己加上api_key=
model = ChatDeepSeek(
    model="deepseek-v4-flash",
    temperature=0.3,
    max_tokens=200,
    #.......
    )

#2、定义消息
#用户消息在langchain里被定义成HumanMessage
#系统提示消息SystemMessage 通常作为第一条消息传入
messages=[
    SystemMessage(content="补全一个故事"),
    HumanMessage(content="一只猫正在___")
]

#3、定义输出解析
parse=StrOutputParser()
# print(parse.invoke(result))

#5、定义链
#注意链的顺序不能错,因为它是按照顺序来执行每一个组件的
chain = model | parse
#执行链条
print(chain.invoke(messages))

1.1.3 方式2:init_chat_model

介绍该方法须先了解一下关于 Runnable 接口中的 .invoke() 调用。该方法是将单个输 入转换为对应的输出。例如对于聊天模型来说,就是根据用户的问题输入,输出相应的答案。

invoke() 方法定义:

abstractmethod invoke(

input: Input,

config: RunnableConfig | None = None,

**kwargs: Any,

) → Output

• input :输入一个 Runnable 实例

• config (默认空):用于 Runnable 的配置。

返回值: • 返回一个 Runnable 实例

class langchain_core.runnables.config.RunnableConfig 常用参数说明

参数名

参数描述

configurable

通过 .configurable_fields()在此 Runnable 或子 Runnable 上配置的属性的运行时值。

run_id

针对此调用运行的跟踪器的唯一标识符。如果未提供,将生成新的 UUID。

run_name

此调用的跟踪器运行的名称。默认为类的名称。

metadata

此次调用和任何子调用的元数据。键是字符串,值是 JSON。
类型:dict[str, Any]

上面的 ChatOpenAI 用于明确创建 OpenAI 聊天模型的实例。而 init_chat_model() 是一个工 厂函数,它可以初始化多种支持的聊天模型(如 OpenAI、Anthropic、FireworksAI 等),不仅仅是 OpenAI 的聊天模型。

init_chat_model() 函数定义

langchain.chat_models.base.init_chat_model(
model: str,
*,
model_provider: str | None = None,
configurable_fields: Literal[None] = None,
config_prefix: str | None = None,
**kwargs: Any,
) → BaseChatModel

参数类别

参数名

参数描述

init_chat_model() 常用参数

model

要使用的模型的名称

model_provider

模型提供方。支持的model_provider值及对应集成包:
- openai→ langchain-openai
- anthropic→ langchain-anthropic
- google_genai→ langchain-google-genai
- ollama→ langchain-ollama
- deepseek→ langchain-deepseek
...
未指定时,将尝试从模型名称推断

configurable_fields

设置可配置的模型参数范围:
- None:无可用配置字段
- 'any':所有字段可配置(如api_keybase_url等运行时修改)
- Union[List[str], Tuple[str,…]]:仅指定字段可配置

config_prefix

配置前缀规则:
- 非空字符串:通过config["configurable"]["{config_prefix}_{param}"]读取配置
- 空字符串:通过config["configurable"]["param"]读取配置

其他模型参数设置

temperature

采样温度:值越高回答越天马行空,值越低越保守靠谱

max_tokens

生成内容的最大令牌数

timeout

请求超时时间

max_retries

最大重试次数

openai_api_key / api_key

OpenAI API密钥;未传入时,自动读取环境变量OPENAI_API_KEY

base_url

API请求的基础URL

……

其他扩展参数(如更多模型专属配置项)

from langchain.chat_models import init_chat_model
from langchain_core.messages import SystemMessage, HumanMessage

#1、基本用法
#LangChain封装了更上层的方法,让我们初始化模型
deepseek_model = init_chat_model(
    model="deepseek-v4-flash",
    model_provider="deepseek",
    temperature=0.3
)

result1 = deepseek_model.invoke("你是谁")
print(f"deepseek-v4-flash:{result1.content}")

#2、定义可配置的模型(模型模拟器)
config_model=init_chat_model(temperature=0.3)
messages=[
    SystemMessage(content="补全一个故事,一百字以内:"),
    HumanMessage(content="一只猫正在__")
]

config={
    "configurable":{
        "model":"deepseek-v4-flash",
        "model_provider":"deepseek"
    }
}

#.invoke()的config参数才正真意义上定义了大模型
result2=config_model.invoke(input=messages,config=config)
print(f"config_model:{result2.content}")

#3、可配置的模型(默认参数)
model=init_chat_model(
    model="deepseek-v4-flash",
    model_provider="deepseek",
    temperature=0.3,
    max_tokens=1024
)
messages=[
    SystemMessage(content="补全故事,100子以内"),
    HumanMessage(content="一只狗正在__")
]

resullt=model.invoke(
    input=messages,
    config={
        "configurable":{}
    }
)

1.2 通过本地部署的 LLM 定义聊天模型

1.2.1 ChatOllama

若想使用 ChatOllama,需要先安装 Ollama 包:

pip install -U langchain_ollama

2. 聊天模型--调用工具

工具调用根本作用是让大语言模型(LLM)具备与外部世界交互的能力。

LLM 本身是一个封闭的知识系统,其能力受限于其训练数据(存在滞后性)和内在的文本生成逻辑。 它无法执行直接计算、查询实时信息、操作数据库或调用任何外部 API。工具调用打破了这层壁垒,其 作用具体体现在:

1. 扩展能力边界:模型可以借助工具完成它自身无法完成的任务,如执行数学计算、搜索网络、查询 数据库等。

2. 保证信息实时性:通过调用搜索工具或数据库查询工具,LLM 可以获取最新的、训练数据中不存在 的信息,避免回答过时或“一本正经地胡说八道”。

3. 处理复杂任务:将一个复杂的用户请求(如“分析我上个月的消费趋势”)分解成多个步骤,并依 次调用不同的工具(如“从数据库获取数据” -> “用 Python 进行数据分析” -> “生成图表”) 来协同完成。协调这件事这更体现在 Agent 智能体上。

4. 连接现有系统:可以将企业内部已有的系统、API 和数据库封装成工具,让 LLM 成为一个用自然语 言驱动的统一接口,极大地提升了自动化和集成能力。

在 LangChain 中,聊天模型提供了额外的功能:工具调用。它能使 LLM 与外部服务、API 和数据库进 行交互。工具调用还可用于从非结构化数据中提取结构化信息并执行各种其他任务。

当我们希望获取当前天气情况,由于 LLM 无法获取实时信息,此时我们就可以借助工具,通 过外部服务进行搜索完成查询:

当我们希望获取数据库表中的数据时

定义工具----->绑定工具----->调用工具分为两步:第一步:工具选择。第二步:调用工具

2.1 创建工具

 关于工具schema

对于工具schema,它将从函数名类型提示文档字符串中获取相关属性,以此 来声明一个工具,包括其名称、描述、输入参数、输出类型等等。这里需要说明的是,若是简单定义 工具,,工具 schema 需要解析 Google 风格的文档字符串去获取【参数描述】

schema 就是数据的“标准答案格式”。LangChain 里的 tool schema,就是告诉大模型这个工具叫什么、干什么、需要哪些参数、参数分别是什么类型。

2.1.1 使用 @tool 装饰器创建工具

有三种方式创建工具

三种方式最终都会被 LangChain 封装成 BaseTool,区别只在于 参数描述(Schema)是如何生成的

无论哪种写法,最终都会得到:

属性

作用

name

工具名称(给 LLM 用)

description

工具用途说明

args_schema

参数 JSON Schema

invoke()

执行工具

在 LangChain 中,实现了一个 @tool 装饰器来创建工具, @tool 装饰器是自定义工具的最简单方 法。

工作原理

LangChain 会:

  1. 用函数名 → name

  2. 用 docstring 第一行 → description

  3. 解析 Args:段落 → 参数描述

  4. 用 Python 类型注解 → 参数类型


 

2.1.1.1 模式1:依赖 Pydantic 类

工作原理

  • 完全由 Pydantic 接管参数定义

  • LangChain 直接读取:

    • model_json_schema()

    • Field(description=...)

为什么依赖 Pydantic 类?

在 LangChain 中,用 @tool装饰器定义工具(给大语言模型调用的功能模块)时,工具需要清晰的输入输出描述(告诉模型“参数是什么、有什么用”)。

如果直接用普通 Python 函数 + @tool,但没写文档字符串(docstring),LangChain 会抛出 ValueError “Function must have a docstring if description not provided”

Pydantic 是 Python 生态中专为数据验证和元数据管理设计的库,它能帮我们解决两个核心问题:

  • 运行时数据验证:确保输入参数的类型、必填项、格式符合预期(比如保证 a必须是 int,否则直接报错,避免后续逻辑出错)。

  • 自动生成工具元数据:通过 Field给字段添加 description(描述)、examples(示例)等元信息,LangChain 会自动提取这些信息,替代 docstring 的作用,让大语言模型能“读懂”工具的参数含义。

步骤 1:导入依赖

需要从 pydantic导入 BaseModelField,从 langchain_core.tools导入 tool

from pydantic import BaseModel, Field
from langchain_core.tools import tool

步骤 2:定义 Pydantic 模型(描述输入结构)

我们要实现一个“加法工具”,输入是两个整数 ab,则创建一个继承 BaseModel的类,用 Field配置每个字段的元数据:

骤 3:用 @tool装饰工具函数,指定 Pydantic 模型为参数类型

工具函数的参数类型设为刚才的 Pydantic 模型,LangChain 会自动解析模型的元数据(如 description):

#方法二
class AddInput(BaseModel):
    """两数相加"""
    a:int=Field(...,description="第一个整数")
    b:int=Field(...,description="第二个整数")
@tool(args_schema=AddInput)
def add(a:int,b:int)->int:
    return a+b

print(add.invoke({"a": 2, "b":3}))
print(add.name)
print(add.description)
print(add.args)
2.1.1.2 模式2:依赖 Annotated

在 LangChain 中,可以依赖 Annotated 和文档字符串传递给工具 Schema 。

LangChain 中,“依赖 Annotated 和文档字符串”是为了自动化/简化“工具 Schema 的定义”

  • Annotated 的作用

    Annotated[类型, 元数据]给工具的参数/返回值附加“描述性元数据”(比如参数含义、取值示例)。这些元数据会被 LangChain 提取,用于填充 Schema 的“说明信息”。

底层机制

  • LangChain 使用 类型提取器

  • 自动生成等价 Pydantic Schema

  • 不需要你手写 BaseModel

#方法三
@tool
def add(
        a : Annotated[int, ..., "第一个整数"],
        b : Annotated[int, ..., "第二个整数"],
)->int:
    """两数相加
    Args:
        a:第一个整数
        b:第二个整数
    """
    return a + b

print(add.invoke({"a": 2, "b":3}))
print(add.name)
print(add.description)
print(add.args)

2.1.2 使用 StructuredTool 类提供的函数创建工具

class langchain_core.tools.structured.StructuredTool 类用来初始化工具,其中 from_function 类方法通过给定的函数来创建并返回一个工具。 from_function 类方法定义如 下:

classmethod from_function(
func: Callable | None = None,
coroutine: Callable[[...], Awaitable[Any]] | None = None,
name: str | None = None,
description: str | None = None,
return_direct: bool = False,
args_schema: type[BaseModel] | dict[str, Any] | None = None,
infer_schema: bool = True,
*,
response_format: Literal['content', 'content_and_artifact'] = 'content',
parse_docstring: bool = False,
error_on_invalid_docstring: bool = False,
**kwargs: Any,
) → StructuredTool

关键参数说明:

• func:要设置的工具函数

• coroutine:协程函数,要设置的异步工具函数

• name:工具名称。默认为函数名称。

• description:工具描述。默认为函数文档字符串。

• args_schema:工具输入参数的schema。默认为 None。

• response_format:工具响应格式。默认为“content”。 ◦ 如果配置为 “content” ,则工具的输出为 ToolMessage 的 content 属性。

▪ 对于 HumanMessage 、 AIMessage 已经见过,分别表示 用户消息 和 AI消息响应 , 对于 ToolMessage ,它表示对应工具角色所发出的消息。 ◦ 如果配置为 “content_and_artifact” ,则输出应是与 ToolMessage 的 content 属性 与 artifact 属性相对应的二元组。(用法见下面的 示例3 )

• 该类方法全部参数及含义见这里from_function | langchain_core | LangChain Reference

2.1.2.1 示例1:常规用法

对于用该类方法创建的工具,同样函数名、类型提示和文档字符串也都是传递给工具 Schema 的一部 分,不可缺失。

from langchain_core.tools import StructuredTool

#方式一
def add(a:int,b:int)->int:
    """两数相加"""
    return a+b
add_tool=StructuredTool.from_function(add)

print(add_tool.invoke({"a": 1, "b": 2}))
print(add_tool.name)
print(add_tool.description)
print(add_tool.args)
2.1.2.2 示例2:加入配置,依赖 Pydantic 类

同样的,让工具函数不提供描述、文档字符串等需要传递给工具 Schema 的内容,如下所示:

此时可以: 1. 使用 args_schema 参数,依赖 Pydantic 类定义并提供工具输入参数的 schema 属性。

2. 使用 description 参数,替代文档字符串中对于工具描述的 schema 属性。


#方法二
class AddInput(BaseModel):
    a:int=Field(description="第一个整数")
    b:int=Field(description="第二个整数")
def add(a:int,b:int)->int:
    return a+b
add_tool=StructuredTool.from_function(
    func=add,
    name="ADD", #工具名
    description="两数相加",#工具描述
    args_schema=AddInput,#工具参数
)

print(add_tool.invoke({"a": 1, "b": 2}))
print(add_tool.name)
print(add_tool.description)
print(add_tool.args)

2.1.2.3 示例3:加入 response_format 配置

如果希望我们的工具区分消息内容(content)和其他工件(artifact),让大模型读取 content , 而一些用来构造 content 的原始数据保存下来,若后续有一些记录、分析的步骤,就可以派上用场 了,这就是 artifact 。 artifact 通常需要使用字典 Dict 或列表 List 保存。我们今后做日志记录、分析,或自定义后续的处理都很方便。

接下来举个例子再来理解下。例如我们定义了一个搜索天气的 tool ,若使用搜索引擎工具询问 “今 天的天气如何?” 时:

• content 可能是: “根据最新搜索结果,今天北京晴,气温在25°C到32°C之间。建议穿短袖衣 物。”

• artifact 可能是某搜索引擎 API 返回的完整 JSON 响应,其中包含多个搜索结果条目、每个条 目的标题、链接、摘要、排名等元数据。

#方法三
class AddInput(BaseModel):
    a:int=Field(description="第一个整数")
    b:int=Field(description="第二个整数")
def add(a:int,b:int)->Tuple[str,List[int]]:
    nums=[a,b]
    content=f"{nums}相加的结果是{a+b}"
    return nums,content
add_tool=StructuredTool.from_function(
    func=add,
    name="ADD", #工具名
    description="两数相加",#工具描述
    args_schema=AddInput,#工具参数
    response_format="content_and_artifact"
)
#模拟大模型调用的姿势
print(add_tool.invoke(
    {"name": "ADD",
     "args": {"a": 1, "b": 2},
     "type": "tool_call",  # 必填
     "id": "111",  # 必填
     }))

小结一下,由于 LLM 大多理解文本,所以工具的主要输出 content 必须是结构良好、简洁的文本, 以便模型能够轻松理解和基于它进行推理、生成下一步的指令。

在链(Chain)中,工具调用之后的其他组件或函数,可能需要工具的原始且结构化数据(即 artifact )来执行特定操作。这些数据可能是庞大的、且非文本的。这些数据不适合直接塞给模 型。因此, artifact 其实是为了给链中后续的组件或函数使用的,不被大模型所直接使用!!

定义工具----->绑定工具----->调用工具分为两步:第一步:工具选择。第二步:调用工具

2.2 绑定工具

为了实际将这些工具绑定到聊天模型,可以使用聊天模型的 .bind_tools() 方法。

使用聊天模型的 .bind_tools() 方法。参数这里不细讲。它的返回值是一个Runnable实例

from typing import Annotated
from langchain_core.tools import tool
from langchain_deepseek import ChatDeepSeek

@tool
def add(
        a : Annotated[int, ..., "第一个整数"],
        b : Annotated[int, ..., "第二个整数"],
)->int:
    """两数相加 """
    return a + b

@tool
def multiply(
        a : Annotated[int, ..., "第一个整数"],
        b : Annotated[int, ..., "第二个整数"],
)->int:
    """两数相乘"""
    return a * b

model=ChatDeepSeek(model="deepseek-v4-flash")
#绑定工具
tools=[add,multiply]
model_with_tools=model.bind_tools(tools=tools)

#调用工具
print(model_with_tools.invoke("2乘3等于多少"))

定义工具----->绑定工具----->调用工具分为两步:第一步:工具选择。第二步:调用工具

2.3 工具调用

通过 .bind_tools() 方法我们可知,它返回了一个 Runnable 实例,因此我们可以使用该 Runnable 实例,调用 .invoke() 方法,完成工具调用。代码在上面,和工具定义写一块儿的

下面是工具的选择步骤

工具调用的一个关键原则是,模型根据输入的相关性决定何时使用工具。模型并不总是需要调用工 具。当你给定一个不相关的输入,模型不会调用该工具。

2.4 强制模型调用工具

#强制绑定工具,tool_choice=""添加参数即可,每个ai的不一样
model_with_tools=model.bind_tools(tools=tools,tool_choice="any")

2.5 工具属性

现在我们知道,输出结果是一个 AIMessage 。但是,如果调用了工具,则 result 将具有一个 tool_calls 属性。此属性包括执行该工具所需的一切,包括工具名称和输入参数,

model_with_tools = model.bind_tools(tools, tool_choice="any")
result = model_with_tools.invoke("9乘6等于多少?")
print(result.tool_calls)

输出结果:
[{'name': 'multiply', 'args': {'a': 9, 'b': 6}, 'id':
'call_csFbMmYD4Dmz8yMja3ZWK1QW', 'type': 'tool_call'}]

2.6 将工具输出传递给聊天模型

2.6.1 为什么需要“将工具输出传递给聊天模型”?

在 LangChain 的工具调用流程里,大模型本身并不会真的执行 Python 函数。

它能做的是:

判断是否需要调用工具
选择要调用哪个工具
生成调用工具需要的参数

真正执行工具的是 Python 程序。

也就是说,大模型和工具之间不是这样:

用户提问 → 大模型自动执行工具 → 大模型直接得到结果 → 回答

而是这样:

用户提问
    ↓
大模型判断需要调用工具
    ↓
大模型返回工具调用请求
    ↓
Python 程序执行工具
    ↓
Python 程序把工具结果传回大模型
    ↓
大模型根据工具结果继续回答

所以重点就是:

工具执行完以后,结果必须重新放回 messages,
再传给聊天模型。

否则,工具结果只存在于 Python 程序里,大模型并不知道工具执行结果是多少。


2.6.2 工具调用中涉及的核心消息类型

在 LangChain 里,对话不是简单字符串,而是由一组消息对象组成。

常见消息类型有:

SystemMessage
HumanMessage
AIMessage
ToolMessage

重点关注三个:

HumanMessage
AIMessage
ToolMessage

2.6.3 HumanMessage:用户消息

HumanMessage 表示用户输入的问题。

例如:

HumanMessage(content="2乘3等于多少?6加6等于多少?")

它表达的意思是:

用户问:2乘3等于多少?6加6等于多少?

在工具调用流程里,HumanMessage 是起点。


2.6.4 AIMessage:模型消息

AIMessage 表示模型返回的消息。

但是要注意:
在工具调用场景下,AIMessage 不一定是最终答案。

它可能有两种情况。

第一种情况:普通回答。

AIMessage:2乘3等于6,6加6等于12。

这种情况下,模型已经完成回答。

第二种情况:工具调用请求。

AIMessage:我需要调用 multiply 工具,参数是 a=2, b=3。

这种情况下,模型还没有最终回答,它只是告诉程序:

我想调用某个工具。

在 LangChain 中,这个工具调用请求会放在:

ai_msg.tool_calls

里面。


2.6.5 tool_calls:模型发出的工具调用请求

当模型认为需要调用工具时,它会在 AIMessage 中生成 tool_calls

例如:

[
    {
        "name": "multiply",
        "args": {"a": 2, "b": 3},
        "id": "call_001",
        "type": "tool_call"
    }
]

这个结构表示:

模型想调用 multiply 工具
传入参数 a=2, b=3
这次工具调用的编号是 call_001

这里要特别注意:

tool_calls 只是“调用请求”,不是“工具执行结果”。

也就是说,模型只是提出:

请帮我算 2 * 3。

但它还没有真正得到结果。


2.6.6 ToolMessage:工具执行结果

当 Python 程序执行工具后,需要把工具结果包装成 ToolMessage

例如模型请求调用:

multiply(a=2, b=3)

Python 程序执行后得到:

6

这个结果要包装成类似这样的消息:

ToolMessage(
    content="6",
    name="multiply",
    tool_call_id="call_001"
)

它表示:

编号为 call_001 的工具调用已经执行完了。
工具 multiply 的返回结果是 6。

这里最重要的是:

tool_call_id="call_001"

因为它用来对应前面 AIMessage 中的工具调用请求。


2.6.7 为什么 ToolMessage 必须传回模型?

工具执行结果最开始只存在于 Python 程序中。

例如:

result = multiply(2, 3)

此时 Python 知道结果是 6,但是大模型不知道。

大模型下一步要想继续回答,就必须看到:

刚才 multiply 工具的结果是 6。

所以需要把工具结果放进 messages

messages.append(tool_msg)

然后再次调用模型:

model_with_tools.invoke(messages)

这就是“将工具输出传递给聊天模型”的真正含义。


2.6.8 工具调用的标准流程

完整流程可以记成下面这条链路:

HumanMessage
用户提出问题

    ↓

AIMessage
模型决定是否调用工具

    ↓

tool_calls
模型给出工具名和参数

    ↓

Python 执行工具
真正调用 add / multiply 等函数

    ↓

ToolMessage
工具执行结果

    ↓

messages.append(tool_msg)
把工具结果加入上下文

    ↓

再次调用聊天模型
让模型读取工具结果并继续回答

如果模型还需要继续调用工具,就继续这个循环。

如果模型不再返回 tool_calls,说明它已经可以给最终答案了。


2.6.9 为什么要循环调用?

模型不一定一次性把所有工具都调用完。

对于这个问题:

2乘3等于多少?6加6等于多少?

模型可能一次性返回两个工具调用:

multiply(2, 3)
add(6, 6)

也可能分两轮返回:

第一轮:multiply(2, 3)
第二轮:add(6, 6)
第三轮:最终回答

所以标准写法不能只处理一次工具调用,而应该循环处理:

只要 AIMessage 里还有 tool_calls,就继续执行工具。
直到 AIMessage 里没有 tool_calls,才输出最终答案。

2.6.10 标准代码示例
from typing import Annotated

from langchain_core.messages import HumanMessage
from langchain_core.tools import tool
from langchain_deepseek import ChatDeepSeek


@tool
def add(
    a: Annotated[int, "第一个整数"],
    b: Annotated[int, "第二个整数"],
) -> int:
    """两数相加"""
    return a + b


@tool
def multiply(
    a: Annotated[int, "第一个整数"],
    b: Annotated[int, "第二个整数"],
) -> int:
    """两数相乘"""
    return a * b


model = ChatDeepSeek(
    model="deepseek-v4-flash",
    extra_body={
        "thinking": {
            "type": "disabled"
        }
    }
)


tools = [add, multiply]

tools_by_name = {
    tool.name: tool
    for tool in tools
}

model_with_tools = model.bind_tools(tools)

messages = [
    HumanMessage(content="2乘3等于多少?6加6等于多少?")
]


while True:
    ai_msg = model_with_tools.invoke(messages)

    messages.append(ai_msg)

    if not ai_msg.tool_calls:
        print("最终答案:")
        print(ai_msg.content)
        break

    for tool_call in ai_msg.tool_calls:
        tool = tools_by_name[tool_call["name"]]
        tool_msg = tool.invoke(tool_call)
        messages.append(tool_msg)

2.6.11 代码第一部分:导入相关模块
from typing import Annotated

Annotated 用来给参数添加说明。

例如:

a: Annotated[int, "第一个整数"]

意思是:

a 是 int 类型
并且它的含义是“第一个整数”

这些参数说明会被 LangChain 转换成工具描述,帮助模型理解参数该怎么填。


from langchain_core.messages import HumanMessage

HumanMessage 表示用户消息。

后面创建用户问题时会用到:

HumanMessage(content="2乘3等于多少?6加6等于多少?")

from langchain_core.tools import tool

tool 是装饰器。

它可以把普通 Python 函数包装成 LangChain 工具。

普通函数:

def add(a, b):
    return a + b

加上 @tool 后:

@tool
def add(a, b):
    return a + b

它就变成了模型可以调用的工具。


from langchain_deepseek import ChatDeepSeek

ChatDeepSeek 是 DeepSeek 聊天模型在 LangChain 中的封装。

通过它可以创建一个聊天模型对象。


2.6.12 代码第二部分:定义工具

先定义加法工具:

@tool
def add(
    a: Annotated[int, "第一个整数"],
    b: Annotated[int, "第二个整数"],
) -> int:
    """两数相加"""
    return a + b

这段代码有几个重点。

第一,@tool 表示把函数注册成工具。

第二,函数名 add 会成为工具名。

第三,文档字符串:

"""两数相加"""

会成为工具描述。

模型会根据这个描述判断:

这个工具适合处理加法问题。

第四,参数说明:

a: Annotated[int, "第一个整数"]
b: Annotated[int, "第二个整数"]

会帮助模型知道这个工具需要两个整数参数。


再定义乘法工具:

@tool
def multiply(
    a: Annotated[int, "第一个整数"],
    b: Annotated[int, "第二个整数"],
) -> int:
    """两数相乘"""
    return a * b

这表示:

工具名:multiply
功能:两数相乘
参数:a 和 b
返回值:a * b

当用户问:

2乘3等于多少?

模型就可能选择调用这个工具。


2.6.13 代码第三部分:创建模型
model = ChatDeepSeek(
    model="deepseek-v4-flash",
    extra_body={
        "thinking": {
            "type": "disabled"
        }
    }
)

这里创建了一个 DeepSeek 聊天模型。

其中:

model="deepseek-v4-flash"

表示使用的模型名称。

下面这部分:

extra_body={
    "thinking": {
        "type": "disabled"
    }
}

表示关闭 thinking 模式。

在学习工具调用时,建议先关闭 thinking 模式,因为某些模型在 thinking 模式下进行多轮工具调用时,可能需要额外处理 reasoning_content,对新手不太友好。

关闭后,工具调用流程更清晰。


2.6.14 代码第四部分:准备工具列表
tools = [add, multiply]

这里把工具放进列表。

这个列表后面会绑定给模型。

意思是告诉模型:

当前可以使用的工具有 add 和 multiply。

2.6.15 代码第五部分:创建工具映射表
tools_by_name = {
    tool.name: tool
    for tool in tools
}

这段代码会生成一个字典。

大致等价于:

tools_by_name = {
    "add": add,
    "multiply": multiply,
}

为什么需要这个字典?

因为模型返回工具调用时,通常只会返回工具名:

tool_call["name"]

比如:

"multiply"

程序需要根据这个名字找到真正的工具对象:

tool = tools_by_name["multiply"]

然后才能执行工具。

所以这个字典的作用是:

根据工具名找到对应的工具函数。

2.6.16 代码第六部分:绑定工具到模型
model_with_tools = model.bind_tools(tools)

这一步非常重要。

原始模型 model 本身不知道有哪些工具可以用。

调用 bind_tools 后,模型才知道:

可以调用 add 工具
可以调用 multiply 工具
每个工具需要什么参数
每个工具的功能是什么

所以后面涉及工具调用时,要使用:

model_with_tools.invoke(messages)

而不是单纯使用:

model.invoke(messages)

2.6.17 代码第七部分:创建初始消息列表
messages = [
    HumanMessage(content="2乘3等于多少?6加6等于多少?")
]

这里创建了消息列表。

当前只有一条消息:

HumanMessage:2乘3等于多少?6加6等于多少?

此时还没有:

AIMessage
ToolMessage
工具调用结果

随着程序运行,messages 会不断增长。


2.6.18 代码第八部分:开始工具调用循环
while True:

这里使用死循环,是因为不知道模型需要调用几轮工具。

模型可能一轮就完成,也可能多轮才完成。

所以需要不断重复:

调用模型
检查是否有 tool_calls
执行工具
把工具结果放回 messages
再次调用模型

直到模型不再返回工具调用。


2.6.19 调用绑定工具的模型
ai_msg = model_with_tools.invoke(messages)

这一步把当前完整的 messages 发给模型。

第一次调用时,messages 只有:

HumanMessage:2乘3等于多少?6加6等于多少?

模型看到问题后,会判断是否需要调用工具。

可能返回:

AIMessage:需要调用 multiply 工具

并在 tool_calls 中生成调用请求。


2.6.20 把 AIMessage 加入 messages
messages.append(ai_msg)

这一步必须做。

因为 AIMessage 中记录了模型刚才的动作:

模型请求调用哪个工具
工具参数是什么
工具调用 ID 是什么

如果不把它放回 messages,后面工具结果就没有对应关系。

此时消息列表可能变成:

HumanMessage:2乘3等于多少?6加6等于多少?
AIMessage:请求调用 multiply(a=2, b=3)

2.6.21 判断是否还有工具调用
if not ai_msg.tool_calls:
    print("最终答案:")
    print(ai_msg.content)
    break

这里判断:

ai_msg.tool_calls

是否为空。

如果为空,说明模型没有请求调用工具。

这通常表示模型已经给出了最终答案。

例如:

2乘3等于6,6加6等于12。

于是打印答案,并结束循环。

如果不为空,说明模型还需要调用工具。


2.6.22 遍历模型请求的工具调用
for tool_call in ai_msg.tool_calls:

因为模型一次可能请求一个工具,也可能请求多个工具,所以要遍历。

例如:

[
    {
        "name": "multiply",
        "args": {"a": 2, "b": 3},
        "id": "call_001"
    }
]

或者:

[
    {
        "name": "multiply",
        "args": {"a": 2, "b": 3},
        "id": "call_001"
    },
    {
        "name": "add",
        "args": {"a": 6, "b": 6},
        "id": "call_002"
    }
]

for 可以同时兼容这两种情况。


2.6.23 根据工具名找到工具
tool = tools_by_name[tool_call["name"]]

假设当前工具调用是:

{
    "name": "multiply",
    "args": {"a": 2, "b": 3},
    "id": "call_001"
}

那么:

tool_call["name"]

就是:

"multiply"

所以:

tool = tools_by_name["multiply"]

就拿到了 multiply 工具。

这一步的作用是:

把模型返回的工具名字,转换成真正可以执行的 Python 工具对象。

2.6.24 执行工具并得到 ToolMessage
tool_msg = tool.invoke(tool_call)

这一步会真正执行工具。

例如当前工具是:

multiply

参数是:

{"a": 2, "b": 3}

那么实际执行的是:

multiply(a=2, b=3)

结果是:

6

LangChain 会把这个结果包装成 ToolMessage

类似:

ToolMessage:multiply 的执行结果是 6

这个 ToolMessage 里面包含:

工具结果
工具名称
工具调用 ID

其中工具调用 ID 用来和之前的 AIMessage.tool_calls 对应。


2.6.25 把 ToolMessage 加入 messages
messages.append(tool_msg)

这一步就是本节最核心的操作。

工具结果必须加入 messages,下一轮模型才能看到。

此时消息列表可能是:

HumanMessage:2乘3等于多少?6加6等于多少?
AIMessage:请求调用 multiply(a=2, b=3)
ToolMessage:multiply 返回 6

然后循环回到开头:

ai_msg = model_with_tools.invoke(messages)

这时模型就能看到:

用户问了什么
模型刚才请求了哪个工具
工具返回了什么结果

于是模型可以继续判断:

还需要调用 add 工具

或者直接给最终答案。


2.6.26 一次完整运行时 messages 的变化

初始状态:

messages = [
    HumanMessage("2乘3等于多少?6加6等于多少?")
]

第一次调用模型后:

messages = [
    HumanMessage("2乘3等于多少?6加6等于多少?"),
    AIMessage(tool_calls=[multiply(2, 3)])
]

执行工具后:

messages = [
    HumanMessage("2乘3等于多少?6加6等于多少?"),
    AIMessage(tool_calls=[multiply(2, 3)]),
    ToolMessage("6")
]

第二次调用模型后,可能变成:

messages = [
    HumanMessage("2乘3等于多少?6加6等于多少?"),
    AIMessage(tool_calls=[multiply(2, 3)]),
    ToolMessage("6"),
    AIMessage(tool_calls=[add(6, 6)])
]

执行加法工具后:

messages = [
    HumanMessage("2乘3等于多少?6加6等于多少?"),
    AIMessage(tool_calls=[multiply(2, 3)]),
    ToolMessage("6"),
    AIMessage(tool_calls=[add(6, 6)]),
    ToolMessage("12")
]

最后一次调用模型后:

AIMessage("2乘3等于6,6加6等于12。")

此时没有 tool_calls,循环结束。


2.6.27 最容易混淆的地方

很多初学者容易把这三件事混在一起:

AIMessage
tool_calls
ToolMessage

它们的区别是:

AIMessage:
模型返回的消息。

tool_calls:
AIMessage 里面的工具调用请求。

ToolMessage:
工具执行后的结果。

可以这样记:

AIMessage 是模型说:“我要调用工具。”

tool_calls 是模型列出的:“调用哪个工具,传什么参数。”

ToolMessage 是程序告诉模型:“工具执行完了,结果是这个。”

2.6.28 另一个容易混淆的地方:content 有内容不代表最终答案

有时候模型返回:

AIMessage(content="我先计算乘法:", tool_calls=[...])

这里 content 是有内容的。

但是它不是最终答案。

因为:

ai_msg.tool_calls

不是空的。

所以判断是否结束,不能看 content,而要看:

if not ai_msg.tool_calls:

只有没有工具调用时,content 才可以看作最终答案。


2.6.29 总结

可以把整个流程记成一句话:

HumanMessage 提问题,
AIMessage 选工具,
Python 执行工具,
ToolMessage 回结果,
messages 传回模型,
直到没有 tool_calls 才结束。

再简化一点:

模型不执行工具,只提出工具调用请求;
程序执行工具,再把结果用 ToolMessage 传回模型。

这就是“将工具输出传递给聊天模型”的本质。

解释:

大模型的价值不在于“亲自计算”,而在于:

理解用户自然语言
判断用户意图
选择合适工具
提取工具参数
决定工具调用顺序
拿到工具结果后组织最终回答

所以大模型更像是:调度员 / 大脑 / 指挥官

工具更像是:手脚 / 计算器 / 数据库 / API / 执行器

大模型到底干了什么?

以这个问题为例:2乘3等于多少?6加6等于多少?大模型不是自己算,而是做了这几件事:

第一步,理解用户意图:用户想做两个数学计算

第二步,识别任务类型:第一个是乘法。第二个是加法。

第三步,选择工具:乘法用 multiply。加法用 add。

第四步,提取参数:

multiply(a=2, b=3)
add(a=6, b=6)

第五步,等程序执行工具。

第六步,拿到工具结果后组织回答:2乘3等于6,6加6等于12。

所以大模型解决的是“理解和调度”问题,程序解决的是“执行”问题。


为什么不让大模型直接回答?

因为大模型直接回答可能会出错。

比如简单的:123456789 × 987654321 等于多少?大模型可能会算错。但是工具不会算错:123456789 * 987654321

所以工具调用的意义是:让大模型负责理解问题;让程序负责可靠执行。这样结合起来更稳。

真实项目里,工具一般不是这种简单函数,而是:

查数据库
调用搜索引擎
查询订单
读取文件
发送邮件
调用支付接口
操作浏览器
查询天气
访问公司内部系统
生成报表

比如:

get_user_order(user_id)
search_docs(query)
send_email(to, subject, body)
query_database(sql)
get_weather(city)

这些事情大模型自己做不了,必须靠工具。

工具调用不是让大模型代替程序,而是让大模型把用户的自然语言需求,转换成程序可以执行的具体动作。

2.7 LangChain 提供的工具

工具也不是全部都需要我们自己手搓,其实 LangChain 官方也已经给我们提供了很多现成的工具 (Tool)和工具包(Toolkit)。 LangChain 提供的工具见这里LangChain Python integrations - Docs by LangChain
 

写好的工具一般都是为了使用 LangChain 中集成的三方组件或工具而 创造的,有搜索、数据库、网页浏览器等相关的工具。

2.7.1 TavilySearch

TavilySearch 类可以支持我们进行搜索,Tavily 是一个专门为 AI 设计的搜索引擎,专为智能体检索与 推理需求量身打造的工具。

Tavily 不仅提供了高度可编程的 API 接口,还具备显著优于传统搜索引擎的上下文相关性理解能力。 能够以结构化、可解析的形式返回搜索结果,便于将检索到的信息直接用于后续的推理、生成或任务 执行流程。

• Tavily官网:https://www.tavily.com/,需魔法使用。登录完成后,新建 API Keys

步骤:

1. 安装 langchain-tavily 包:pip install -U langchain-tavily

2. 配置环境变量 TAVILY_API_KEY ,值为我们申请的 API Key:

3. 代码接入 TavilySearch 类,实现搜索功能:

from langchain_core.messages import HumanMessage
from langchain_deepseek import ChatDeepSeek
from langchain_tavily import TavilySearch


# 1. 创建模型,并关闭 thinking mode
model = ChatDeepSeek(
    model="deepseek-v4-flash",
    extra_body={
        "thinking": {
            "type": "disabled"
        }
    }
)

# 2. 创建工具
search_tool = TavilySearch(max_retries=4)
# 3. 工具列表
tools = [search_tool]
# 4. 工具名字到工具对象的映射
tools_by_name = {
    tool.name: tool
    for tool in tools
}
# 5. 绑定工具
model_with_tools = model.bind_tools(tools)
# 6. 初始用户消息
messages = [
    HumanMessage(content="贵州贵阳花溪区今天的天气怎么样?")
]
# 7. 循环:模型 -> 工具 -> 模型
while True:
    # 调用绑定了工具的模型
    ai_message = model_with_tools.invoke(messages)
    # 保存模型返回的 AIMessage
    messages.append(ai_message)
    # 如果模型没有继续调用工具,说明已经得到最终答案
    if not ai_message.tool_calls:
        print(ai_message.content)
        break
    # 如果模型请求调用工具,就逐个执行
    for tool_call in ai_message.tool_calls:
        tool = tools_by_name[tool_call["name"]]
        tool_message = tool.invoke(tool_call)
        # 把工具结果放回 messages
        messages.append(tool_message)

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐