一、什么是聊天模型

 

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

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

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

所以,在LangChain中,聊天模型(Chat Model)是一类专门为多轮对话场景设计的模型接口。它和传统的LLM(如旧版的text-davinci-003)有本质区别:

传统 LLM 聊天模型
输入:一段纯文本字符串 输入:一个消息列表(List of Messages)
输出:一段纯文本字符串 输出:一条消息(AIMessage)
不区分角色,需手动拼接对话 清晰区分 System、Human、AI 等角色

聊天模型理解“对话”的结构,而不仅仅是文本。这使得它更容易处理指令、历史记录和上下文。

聊天模型的基石:四种消息类型

所有对话都由以下消息对象组成,它们存在于 langchain_core.messages

  • SystemMessage:设定 AI 的行为准则、角色身份。一般放在消息列表的最开头。

  • HumanMessage:代表用户输入的任何内容。

  • AIMessage:模型的回复。除了内容文本,还可以携带 tool_calls(工具调用请求)。

  • ToolMessage:用于将工具的执行结果返回给模型,必须包含与调用对应的 tool_call_id

这四种消息就是后续组合一切对话的“原子”。先记住它们的名字和分工,后面的代码会反复出现。


二、API定义聊天模型:如何创建一个模型实例

 理解了概念,我们来写第一行代码——通过 API 初始化一个聊天模型。这里以 deepseek 为例,但 LangChain 的切换成本极低,后面想用 OPenAI 也可以随时更改。

 安装依赖:

pip install langchain langchain-core langchain-deepseek

初始化模型:

from langchain_deepseek import ChatDeepSeek

# 1、定义⼤模型 默认从系统环境变量中读取 DEEPSEEK_API_KEY
chat = ChatDeepSeek(
    model="deepseek-chat",      # 性价比很高的模型
    temperature=0.7,          # 创造性:0为极度确定,1为天马行空
    max_tokens=500,           # 控制最大输出长度
    request_timeout=30,       # 请求超时
    max_retries=2             # 失败自动重试
)

三、定义工具

有了模型后,它还只能“说话”。如果我们想让它查天气、算数学题或操作数据库,就需要定义工具(Tools)。工具的本质就是一个普通 Python 函数 + 一点描述信息。

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

LangChain 提供了 @tool 装饰器来快速创建,这是自定义工具最简单的方法。

工具有工具名称工具描述工具参数三种属性,这是创建工具的底层逻辑。

对于工具来说:

  1. 工具名称可以让LLM知道有哪些工具,可以调用哪些工具

  2. 工具描述实际上就是在写提示词,告诉模型工具的能力,让模型知道调谁

  3. 工具参数可以让模型知道怎么调

3.1.1 模式一:依赖 Pydantic 类

若使用 @tool 定义工具时,没有提供文档字符串,则会报错:

ValueError: Function must have a docstring if description not provided

此时,在 LangChain 中,可以使用 Pydantic 类提供运行时数据验证和类型检查。通过Field(description="...") 添加字段描述,LangChain 会自动提取对应的信息。

注意代码中 @tool 的 args_schema 参数,它表示工具函数在未提供描述、文档字符串等需要传递给工具 Schema 的内容时,依赖 Pydantic 类使用 args_schema 参数,定义并提供工具输入参数的schema。默认为 None。点击运行,不会报错,且将来运行时会进行数据验证。因此,再次印证了函数名、类型提示和文档字符串都是传递给工具 Schema 的一部分,不可缺失。

3.1.2 模式2:依赖 Annotated

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

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

# 导入 LangChain 专门用来创建「结构化工具」的类
from langchain_core.tools import StructuredTool

# 定义一个普通的 Python 函数:两数相乘
def multiply(a: int, b: int) -> int:
    """Multiply two numbers."""  # 文档字符串,大模型会看懂这个功能
    return a * b

# 重点:把普通函数 → 变成 LangChain 能用的「工具」
calculator_tool = StructuredTool.from_function(func=multiply)

# 调用这个工具(传入字典格式的参数)
print(calculator_tool.invoke({"a": 2, b": 3}))  # 输出 6
  • StructuredTool 是 LangChain 提供的工具类
  • 它里面有一个类方法from_function
  • 这个方法的作用就是:传入一个普通函数 → 自动生成一个可被大模型使用的工具
3.2.1 加入配置,依赖 Pydantic 类

使用 Pydantic 类让工具函数不提供描述、文档字符串等需要传递给工具 Schema 的内容:

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

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

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


class CaCalculatorInput(BaseModel):
    a: int = Field(description="first number")
    b: int = Field(description="second number")

def multiply(a: int, b: int) -> int:
    return a * b

calculator_tool = StructuredTool.from_function(
    func = multiply,
    name = "Calculator",
    description="两数相乘",
    args_schema=CaCalculatorInput,
)
print(calculator_tool.invoke({"a": 3, "b": 2}))
print(calculator_tool.name)
print(calculator_tool.description)

四、绑定工具

定义工具后,要把工具列表到聊天模型上。可以使用聊天模型的 bind_tools() 方法,绑定之后,模型在一次推理中如果认为需要,就会在返回的 AIMessage 里附带 tool_calls

例如:

from langchain_deepseek import ChatDeepSeek

# 定义大模型
model = ChatDeepSeek(model="deepseek-v4-flash")

# 绑定工具,返回一个 Runnable 实例
tools = [add, multiply]
model_with_tools = model.bind_tools(tools)

bind_tools() 会做两件事:

  1. 将工具的名称、描述和参数 schema 注入到模型的系统指令中(或者通过特定 API 参数)。

  2. 返回一个新的可运行对象,之后的调用都将携带这个工具列表信息。

现在 chat_with_tools 就是一个知道“我身边有两个工具可用”的聊天模型了。


五、调用工具

 #调用工具
response = model_with_tools.invoke("3 + 5 等于多少?")
print(response)

模型通过response的内容来决定调用哪个工具,比如 "3 + 5 等于多少?"的话模型就会调用 add 工具。

模型并没有回答等于 8 ,而是表达了一个调用请求。它说:“我需要调用 add,参数是  3 和 5”。


六、工具属性

在 LangChain 中,每个工具都是 BaseTool 的实例,具有以下重要属性:

  • name:唯一标识。模型会在 tool_calls 中引用这个名字。

  • description:功能说明。务必写得清晰、具体。坏的描述会让模型在错误的时机调用。

  • args_schema:一个 Pydantic 模型,定义了参数的名称、类型、是否必填、默认值等。模型会严格按照它来生成 JSON 参数。

  • return_direct:如果设为 True,工具执行后直接返回结果给用户,不再经过模型润色(在 Agent 中常用)。

例如,我们可以用更精细的方式定义工具:

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

class WeatherInput(BaseModel):
    city: str = Field(description="城市名称,例如'北京'")

def get_weather_func(city: str) -> str:
    weather_db = {"北京": "霾,5°C"}
    return weather_db.get(city, "未知")

get_weather_tool = StructuredTool.from_function(
    func=get_weather_func,
    name="get_weather",
    description="查询指定城市的实时天气",
    args_schema=WeatherInput
)

当模型看到 args_schema 定义的字段和描述后,就能准确地填入 {"city": "北京"}


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

到这里可以发现,我们仅仅只是成功调用了工具,但是聊天模型并没有给我们返回我们真正需要的答案。此时就需要:

  1. 将工具输出传递给聊天模型,包括 HumanMessage、AIMessage(工具调用)、ToolMessage

  2. 聊天模型根据以上消息输入,将最终结果 AIMessage 返回

为什么要发 ToolMessage 呢?

之前我们讲过,聊天模型通常不是接受单个字符串作为输入,而是接受聊天消息(XxxMessage)列表,因此在这里我们需要将工具的返回,构造成 ToolMessage,再传输给聊天模型!!!方便的是,如果我们使用 @tool 装饰器创建的工具,使用 tool.invoke(tool_calls),将自动返回一个 ToolMessage。完整示例如下:

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

# 定义大模型
model = ChatDeepSeek(model="deepseek-chat")

# 定义工具
@tool
def add(
    a: Annotated[int, ..., "First integer"],
    b: Annotated[int, ..., "Second integer"]
) -> int:
    """Add two integers."""
    return a + b

@tool
def multiply(
    a: Annotated[int, ..., "First integer"],
    b: Annotated[int, ..., "Second integer"]
) -> int:
    """Multiply two integers."""
    return a * b

# 绑定工具
tools = [add, multiply]
model_with_tools = model.bind_tools(tools)

# 添加AIMessage到消息中
messages = [
    HumanMessage("9乘6等于多少?5加3等于多少? ")
]
ai_msg = model_with_tools.invoke(messages)
messages.append(ai_msg)

for tool_call in ai_msg.tool_calls:
    # 根据工具名选择对应工具函数(不区分大小写)
    selected_tool = {"add": add, "multiply": multiply}[tool_call["name"].lower()]
    # 执行工具调用,返回 ToolMessage
    tool_msg = selected_tool.invoke(tool_call)
    # 将 ToolMessage 加入消息
    messages.append(tool_msg)

print(messages)

result = model.invoke(messages)
print(result)


八、结构化输出

很多时候,我们不希望模型的最终回答是一段纯文本,而是希望得到 JSON、对象等结构化数据,方便程序处理。这就需要输出解析器

8.1 StrOutputParser —— 提取纯文本字符串

StrOutputParserLangChain 框架内置的基础输出解析器,实现了 BaseOutputParser 标准接口,专用于将大模型返回的 AIMessage 消息对象,解析为纯字符串(str)类型

from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_deepseek import ChatDeepSeek

# 定义模型
model = ChatDeepSeek(model="deepseek-chat")

# 提示词模板
prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个翻译助手"),
    ("human", "将'{text}'翻译成英文")
])
chain = prompt | model | StrOutputParser()
text = chain.invoke({"text": "你好世界"})
print(text)  # "Hello World"
  • prompt:生成标准化提示词
  • chat:大模型执行推理,返回 AIMessage 对象
  • StrOutputParser:将 AIMessage 解析为纯字符串,完成最终输出

输入参数

        ↓

prompt 生成对话消息

        ↓

模型返回 AIMessage(复杂对象)

        ↓

StrOutputParser 提取 .content  →  转为 str

        ↓

得到纯文本字符串

8.2 PydanticOutputParser —— 最强大的结构化输出

利用 Pydantic 定义想要的输出格式,解析器会自动生成格式指令并校验输出。

from langchain_core.output_parsers import PydanticOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_deepseek import ChatDeepSeek
from pydantic import BaseModel, Field

model = ChatDeepSeek(model="deepseek-chat")

class Person(BaseModel):
    name: str = Field(description="姓名")
    age: int = Field(description="年龄")
    email: str = Field(description="邮箱")

parser = PydanticOutputParser(pydantic_object=Person)

prompt = ChatPromptTemplate.from_messages([
    ("system", "提取用户信息,按指定格式输出。{format_instructions}"),
    ("human", "我叫李明,28岁,邮箱是liming@example.com")
])
prompt = prompt.partial(format_instructions=parser.get_format_instructions())

chain = prompt | model| parser
person_obj = chain.invoke({})
print(person_obj.name)  # 李明
print(person_obj.age)   # 28

parser.get_format_instructions() 会生成类似这样的指令:

The output should be formatted as a JSON instance that conforms to the JSON schema below...
{"properties": {"name": {"description": "姓名", "type": "string"}, ...}}

模型读取这段指令后,会直接输出符合该 JSON Schema 的字符串,解析器再将其转化为 Pydantic 对象。如果格式不符,解析器会抛出异常,可以在链中配置重试。

注意: 在支持原生 function calling 的模型上,也可以结合工具调用获取结构化输出,但 PydanticOutputParser 依然是适用范围最广的通用方案。


九、流式传输

聊天类应用通常需要逐字显示回复,提升用户体验。LangChain 提供了 stream 方法。

messages = [
    ("system", "你是一个诗人,作答要简洁。"),
    ("human", "写一首关于海的三行诗")
]

for chunk in chat.stream(messages):
    # chunk 是一个 AIMessageChunk 对象,content 包含增量文本
    print(chunk.content, end="", flush=True)

输出结果:

波涛轻吟着古老的歌,
月光洒下银色的故事,
远方是梦的彼岸。

stream 返回的是一个生成器,每次 yield 一个 token 块。也可以结合异步环境使用 astream

async for chunk in chat.astream(messages):
    print(chunk.content, end="", flush=True)

如果工具调用过程中也需要流式输出,通常我们在工具调用循环的最终回答生成阶段使用 stream,而不是在第一次(可能产生 tool_calls 的)调用中流式,因为 tool_calls 需要完整返回才能解析。


十、总结

把以上所有流程串联,写一个终端运行的、带工具调用和流式回答的聊天机器人。

import os
from langchain_deepseek import ChatDeepSeek
from langchain_core.messages import HumanMessage, AIMessage, ToolMessage
from langchain_core.tools import tool

# ---------- 1. API定义模型 ----------
chat = ChatDeepSeek(model="deepseek-chat", temperature=0)


# ---------- 2. 定义工具 ----------
@tool
def get_weather(city: str) -> str:
    """查询指定城市天气"""
    db = {"北京": "霾 5°C", "上海": "小雨 12°C"}
    return db.get(city, "无数据")


@tool
def calculator(exp: str) -> str:
    """计算数学表达式"""
    return str(eval(exp))


# ---------- 3. 绑定工具 ----------
chat_with_tools = chat.bind_tools([get_weather, calculator])

# ---------- 4. 交互循环 ----------
messages = []
print("🤖 聊天机器人启动(输入 quit 退出)")
while True:
    user_input = input("你:")
    if user_input.lower() == "quit":
        break
    messages.append(HumanMessage(content=user_input))

    # 首次调用(可能产生工具调用)
    response = chat_with_tools.invoke(messages)

    # 处理工具调用
    while response.tool_calls:
        messages.append(response)  # 将AIMessage加入历史
        for tc in response.tool_calls:
            if tc["name"] == "get_weather":
                result = get_weather.invoke(tc["args"])
            elif tc["name"] == "calculator":
                result = calculator.invoke(tc["args"])
            else:
                result = "未知工具"
            messages.append(ToolMessage(content=result, tool_call_id=tc["id"]))
        # 再次调用模型
        response = chat_with_tools.invoke(messages)

    # 模型给出最终文本回答,并进行流式输出
    print("AI:", end="")
    # messages 中已经包含了本次所有上下文,但我们也可以用 stream 重新生成最终回答
    # 简便起见直接打印 content,流式演示我们另外模拟:
    full = chat.invoke(messages)
    print(full.content)
    messages.append(full)  # 将最终回答也加入历史

代码核心模块拆解:

  1. 模型初始化 :导入依赖,连接 DeepSeek 大模型,设置低随机性,保证回答稳定。

  2. 自定义工具 :

    • 天气工具:内置固定城市天气数据,用于查询天气;

    • 计算工具:借助eval执行数学表达式运算;

    • 通过@tool装饰器标记,让模型识别为可调用函数。

  3. 工具绑定 :将两个自定义工具绑定给大模型,赋予模型主动调用工具的能力。

  4. 循环对话逻辑 :

    • 开启无限聊天循环,收集用户输入,输入quit即可退出;

    • 用列表存储完整对话记录,实现上下文连贯;

  5. 自动工具调用核心 :

    • 模型首次接收问题后,判断是否需要调用工具;

    • 若需要,代码自动执行对应工具、获取结果并回填对话记录;

    • 再次请求大模型,让模型结合工具返回的数据,生成最终回答。

  6. 结果输出 :打印 AI 最终回复,并将 AI 回答存入上下文,维持多轮对话连贯
Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐