文章目录

零基础入门 LangChain 与 LangGraph(四):聊天模型进阶——工具调用、结构化输出、流式传输与 LangSmith

💬 开篇说明:上一篇我们已经把环境搭建、基础安装和第一个 LangChain 程序跑通了。这一篇不再停留在“能不能调起模型”,而是继续往前走,真正进入 LangChain 聊天模型最关键的几项能力:工具调用、结构化输出、流式传输,以及 LangSmith 跟踪调试

👍 这一篇的定位:如果说第三篇解决的是“LangChain 到底怎么启动”,那这一篇解决的就是“LangChain 为什么开始变得像一个框架”。

🚀 这一篇的目标:写完之后,至少要把下面这些事情搞明白:

  1. 为什么模型、输出解析器、链都能统一用 invoke()
  2. 工具调用到底是怎么完成一次完整闭环的
  3. 结构化输出为什么能把“字符串”变成“对象”
  4. 流式传输背后的真实机制是什么
  5. LangSmith 到底在调试链路里扮演什么角色

一、回顾

上篇我们引出了这两个核心抽象:

  • Runnable
  • LCEL

也粗略解释了:

为什么模型能 invoke(),解析器也能 invoke(),链组合之后居然还能继续 invoke()


1.1 先把最小链路再回顾一遍

这一篇依然从一段最小可运行代码开始,但这次我们看它时,不再只是把它当作入门代码,而是把它当作后续所有能力的起点。

from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_core.output_parsers import StrOutputParser

# 定义大模型
model = ChatOpenAI(model="gpt-5-mini")

# 定义消息列表
messages = [
    SystemMessage(content="Translate the following from English into Chinese"),
    HumanMessage(content="hi!"),
]

# 直接调用模型
result = model.invoke(messages)
print(result)

# 输出解析器
parser = StrOutputParser()
print(parser.invoke(result))

# 链式执行
chain = model | parser
print(chain.invoke(messages))

这段代码看起来很短,但其实已经把 LangChain 的几个关键角色都串起来了:

  • ChatOpenAI:聊天模型
  • SystemMessage / HumanMessage:消息系统
  • StrOutputParser:输出解析器
  • model | parser:链式组合

1.2 API Key 仍然建议走环境变量

这一点上一篇已经讲过,这里只做简短回扣。
聊天模型接入最基本的一步,仍然是先申请 API Key,然后把 Key 放进环境变量,而不是写死在代码里。

如果只是在当前 PowerShell 会话中临时设置:

$env:OPENAI_API_KEY="你的 OpenAI API Key"

如果想写入用户环境变量:

setx OPENAI_API_KEY "你的 OpenAI API Key"

之所以反复强调这件事,是因为它不只是“安全问题”,更是工程习惯问题。代码应该是代码,配置应该是配置。
尤其是后面当你开始同时接 OpenAI、DeepSeek、Ollama、Tavily、LangSmith 时,这种边界感会越来越重要。


1.3 聊天模型的输入,不是字符串优先,而是消息优先

虽然我们平时口头上总说“给模型发一句话”,但在 LangChain 的聊天模型接口里,更自然的输入形式其实不是单个字符串,而是消息列表

from langchain_core.messages import HumanMessage, SystemMessage

messages = [
    SystemMessage(content="Translate the following from English into Chinese"),
    HumanMessage(content="hi!"),
]

这里至少先要认识两种消息:

  • SystemMessage:系统角色消息,用来规定模型的工作方式
  • HumanMessage:用户角色消息,表示用户输入

后面你会接触到的还有:

  • AIMessage:模型返回的消息
  • ToolMessage:工具返回的消息

这个“消息系统”特别重要,因为后面无论是多轮对话、工具调用、结构化输出还是 Agent,其实都建立在这个输入输出形态之上。


1.4 invoke() 返回的不是字符串,而是 AIMessage

调用模型最常见的方式还是:

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

这里返回值不是普通字符串,而是一个 AIMessage
它除了包含真正的文本内容之外,还往往带着一大堆元数据。比如:

  • content:消息内容本身
  • additional_kwargs:额外有效载荷,比如工具调用相关信息
  • response_metadata:响应元数据,比如模型名、完成原因、请求 ID
  • usage_metadata:使用量统计,比如 token 数

这一步最值得建立的习惯是:
不要只盯着 .content,而要先把完整对象打印出来看一眼。

因为以后你在做:

  • 工具调用
  • 结构化输出
  • 流式消息块处理
  • 调试链路

都会频繁碰到这些附加字段。
如果一开始就只把它当成“字符串外壳”,后面一到复杂场景就很容易乱。


1.5 输出解析器的意义,不只是“取出字符串”

如果我只想拿模型最终回答的文本,而不关心完整消息对象,就可以加上 StrOutputParser

from langchain_core.output_parsers import StrOutputParser

parser = StrOutputParser()
print(parser.invoke(result))

很多人第一次看到这里,会觉得这只是“帮我少写一步 .content”。
但真正重要的意义是:

LangChain 把“模型输出之后的处理”也标准化成了一个组件。

也就是说,模型不是这个流程的终点。
模型输出之后,还可以继续过:

  • 字符串解析器
  • JSON 解析器
  • Pydantic 解析器
  • 自定义后处理逻辑

这也是为什么 LangChain 后面会逐步形成“组件 → 组件 → 组件”的整体感。


1.6 链式执行真正说明了什么

最值得注意的一句代码,是:

chain = model | parser
result = chain.invoke(messages)
print(result)

这句代码表面上是在做“链式执行”,但它背后其实已经暴露了 LangChain 的底层抽象能力:

  • 模型能执行
  • 解析器也能执行
  • 它们拼起来之后,新的对象居然还可以继续执行

这意味着,这几类对象一定共享了一套共同接口。

这套接口,就是下一节要说的 Runnable


二、Runnable:LangChain 最重要的统一抽象

如果用一句话概括 Runnable那就是

Runnable 是 LangChain 里“可执行组件”的统一接口。

你只要理解了 Runnable,后面很多“为什么这玩意儿也能 invoke()”的问题就全解释通了。


2.1 什么叫组件(Components)

在这部分材料里,LangChain 的组件可以简单理解成:

构建 LLM 应用时可复用的核心构建块

比如下面这些都可以算组件:

  • 语言模型
  • 输出解析器
  • 检索器
  • 工具
  • 编译后的 LangGraph 图

也就是说,LangChain 并不是在给你一套单一写法,而是在给你一堆可以拼装的“模块”。


2.2 Runnable 提供了哪些标准能力

Runnable 之所以重要,是因为它给这些组件定义了一套统一动作。至少可以先记住下面这几种:

1. invoke:单次调用

这是最常用的能力。
输入一个值,输出一个结果。

result = model.invoke(messages)

2. batch:批处理

不是一个输入,而是一批输入一起处理。


3. stream:流式传输

不是等结果一次性全部回来,而是边生成边返回。


4. inspect:检查输入输出与配置

允许我们查看 Runnable 的输入输出、配置等信息。


5. compose:组合

允许把多个 Runnable 组合起来,形成更复杂的处理管道。


2.3 为什么 modelparser 都能 invoke()

到这里回头再看前面的代码,就很容易理解了:

model.invoke(messages)
parser.invoke(result)

原因并不是“它们恰好都定义了同名方法”,而是:

聊天模型和输出解析器本质上都是 Runnable 接口的实例。

这件事非常关键。
因为它意味着 LangChain 不是在让你记住一堆彼此毫无关系的类,而是在围绕一套统一协议组织生态。

你以后见到:

  • 聊天模型
  • 提示模板
  • 输出解析器
  • 工具
  • 检索器

都会越来越有这种感觉:它们虽然类型不同,但写法却越来越统一。


2.4 Runnable 真正解决的,是“统一交互方式”

如果把这个思想换成更工程化的话来表达,那就是:

只要一个对象实现了 Runnable 接口,它就可以像 LangChain 生态中的其他组件一样,被调用、被组合、被流式处理。

这就是为什么 LangChain 一旦进入实战,写起来会越来越像“搭积木”,而不是像原生 API 那样到处手搓流程控制。


三、LCEL:LangChain 的声明式组合语言

Runnable 解决的是“组件怎么统一执行”。
那下一个问题自然就是:

组件之间怎么优雅地串起来?

答案就是:LCEL(LangChain Expression Language)


3.1 model | parser 不只是写法好看

先回看这句:

chain = model | parser

很多人第一次看到会觉得,这只是一个语法糖。
但实际上,这句表达的意思很明确:

  • 前一个 Runnable 的输出
  • 作为后一个 Runnable 的输入
  • 最终组合成一个新的 Runnable

所以这里最重要的不是符号本身,而是它背后的语义:

LCEL 允许我们用声明式方式,把现有 Runnable 组合成新的 Runnable。


3.2 组合后的对象,叫 RunnableSequence

通过 LCEL 构建出来的这个新对象,通常就叫:

RunnableSequence

也就是“可运行序列”。

这其实就是我们日常说的“链”。
而且最关键的是:

RunnableSequence 本身依然还是 Runnable。

所以你依然可以这样调用它:

chain.invoke(messages)

3.3 | 运算符背后可以理解成什么

从更直观的角度看:

chain = model | parser

本质上就相当于:

from langchain_core.runnables import RunnableSequence

chain = RunnableSequence(first=model, last=parser)

也可以写成:

chain = model.pipe(parser)

之所以用 | 这个符号,其实很容易理解。它借用了 Unix/Linux 里“管道”的直觉:

A | B

前一个过程的输出,变成后一个过程的输入。
在 LangChain 里,这个直觉被完整迁移过来了。


3.4 真正要记住的,不是语法,而是思想

LCEL 不是为了让代码看起来漂亮,而是为了把组件编排这件事,做成声明式、标准化、可组合的语言。

这就是为什么 LangChain 后面会特别强调:

  • 并行
  • 组合
  • 可流式
  • 可复用

因为这些能力背后都离不开 Runnable + LCEL 这套基础。


四、聊天模型的两种定义方式:具体类与统一工厂

把 Runnable 和 LCEL 看明白之后,下一步就该回到聊天模型本身了。

LangChain 到底是怎么组织“不同模型提供方”的聊天模型接口的?


4.1 方式一:直接用 ChatOpenAI

这是最直观、也最适合入门的方式。

from langchain_openai import ChatOpenAI

model = ChatOpenAI(
    model="gpt-5-mini",
    temperature=0,
    max_tokens=None,
    timeout=None,
    max_retries=2,
)

这里常见参数主要有:

  • model:模型名称
  • temperature:采样温度,越高越发散,越低越稳定
  • max_tokens:最大生成长度
  • timeout:请求超时时间
  • max_retries:最大重试次数
  • api_key / openai_api_key:API Key
  • base_url:接口基础地址
  • organization:组织 ID

这种写法的好处是很明显的:
具体、直接、好理解。

它特别适合刚入门时先建立这种直觉:

  • 一个模型对象到底长什么样
  • 参数该写在哪里
  • invoke() 到底是怎样触发调用的

4.2 用兼容 OpenAI 接口的方式接其他模型

有些模型服务会提供 OpenAI 兼容接口。
像 DeepSeek 就先用这种方式做了一个兼容接法:

import os
from langchain_openai import ChatOpenAI

OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")

model = ChatOpenAI(
    base_url="https://api.deepseek.com/v1",
    openai_api_key=OPENAI_API_KEY,
    model="deepseek-chat",
)

这种做法的意义在于:让你不用学新 API,直接用 OpenAI 的写法,就能接别的模型

但要注意,这更适合作为快速接入的思路。
真到了正式工程阶段,如果某个 provider 已经有自己的专用 LangChain 集成包,那优先用专用包,往往会更清晰、更完整。


4.3 方式二:init_chat_model() 统一工厂

如果说 ChatOpenAI 是具体实现类,
init_chat_model() 就更像一个统一工厂。

from langchain.chat_models import init_chat_model

gpt_model = init_chat_model(
    "gpt-5-mini",
    model_provider="openai",
    temperature=0
)

deepseek_model = init_chat_model(
    "deepseek-chat",
    model_provider="deepseek",
    temperature=0
)

这个函数最大的价值在于:

它把不同 provider 的聊天模型初始化方式,统一到了一个更高层入口里。


4.4 init_chat_model() 为什么值得掌握

真正写工程时,你很快就会遇到这些需求:

  • 我想对比不同 provider 的效果
  • 我不希望业务代码死绑某一家模型
  • 我想通过配置切换模型,而不是改一堆初始化代码

这时候,统一工厂的意义就体现出来了。

而且 init_chat_model() 还支持做“运行时可配置模型”。
也就是说,模型名、provider、温度之类的东西,甚至可以在调用时动态注入,而不是在初始化时写死。

这个能力一旦进入稍微复杂的系统,就会非常好用。


4.5 本地部署聊天模型:ChatOllama

除了 API 接入之外,本地模型还有一种常见接法:Ollama

from langchain_ollama import ChatOllama

ollama_model = ChatOllama(
    model="deepseek-r1:70b",
    base_url="http://192.168.100.220:11434"
)

result = ollama_model.invoke("what's your name?")
print(result)

ChatOllama 的意义在于:
它让“本地部署模型”也能进入 LangChain 那套统一抽象里。

也就是说,不管你接的是:

  • OpenAI
  • DeepSeek
  • Ollama 本地模型

只要进入 LangChain 的聊天模型接口,后续使用方式就会越来越统一。

这正是框架抽象的价值所在。


五、工具调用:让模型真正具备与外部世界交互的能力

到这里,聊天模型已经能“说话”了。
但光会说话远远不够。

因为模型本身其实是一个封闭知识系统
它有训练时学到的知识,也有生成文本的能力,但它天然做不到这些事:

  • 查询实时天气
  • 搜索最新网页
  • 操作数据库
  • 调用外部 API
  • 执行函数计算
  • 连接企业内部系统

而工具调用(Tool Calling)的本质,就是把这层边界打破。


5.1 为什么工具调用这么重要

工具调用的意义,可以概括成四点:

1. 扩展能力边界

模型自己不会算复杂表达式,也不会查数据库,但它可以借助工具完成这些事。

2. 获取实时信息

训练数据一定有滞后性。
只靠模型自己,很容易一本正经地回答旧信息。工具则可以把最新信息拉进来。

3. 处理复杂任务

一个用户请求往往不是一步就能做完。工具可以把任务拆成若干个步骤,再逐个调用外部能力完成。

4. 连接现有系统

这对企业场景尤其重要。企业已有的 API、数据库、内部服务,都可以封装成工具,然后由大模型用自然语言驱动。


5.2 用 @tool 装饰器创建工具

在 LangChain 里,定义自定义工具最简单的方法就是 @tool

from langchain_core.tools import tool

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

print(multiply.invoke({"a": 2, "b": 3}))

这段代码非常重要,因为它直接暴露了“工具声明”在 LangChain 里的几个关键来源:

  • 函数名:工具名
  • 类型提示:参数结构
  • 文档字符串:工具描述

也就是说,LangChain 不是把 Python 函数原样塞给模型,而是会从这些信息里提炼出一个工具 Schema


5.3 什么是 Schema,为什么工具离不开它

Schema 可以简单理解成:

描述数据结构的声明格式

说得更直白一点,就是:

  • 这个工具叫什么
  • 它收什么参数
  • 参数叫什么名字
  • 参数是什么类型
  • 每个参数是干什么的
  • 最终应该返回什么形式

模型之所以能“调用工具”,不是因为它真的会执行 Python 代码,而是因为你先把工具描述成了一个清晰的结构,模型再根据这个结构生成“我要调用哪个工具、传什么参数”的请求。

所以函数名、类型提示和文档字符串,并不是“可写可不写的装饰”,而是工具能不能被模型正确理解的前提。


5.4 为什么文档字符串最好写成 Google 风格

如果是简单用 @tool 创建工具,LangChain 会去解析文档字符串里的参数说明。

为了让解析更稳定,最常见的写法就是 Google 风格文档字符串:

def fetch_data(url, retries=3):
    """从给定的URL获取数据。
    Args:
        url (str): 要从中获取数据的URL。
        retries (int, optional): 失败时重试的次数。默认为3。
    Returns:
        dict: 从URL解析的JSON响应。
    """

这不是形式主义。
对工具来说,描述本身就是工具能力的一部分。描述越清楚,模型越容易正确选用工具。


5.5 不写文档字符串怎么办:Pydantic 与 Annotated

如果你不给工具写文档字符串,LangChain 会报错。
这时候就可以借助别的方式把 Schema 信息补上。

方式一:Pydantic 类
from pydantic import BaseModel, Field
from langchain_core.tools import tool

class AddInput(BaseModel):
    """Add two integers."""
    a: int = Field(..., description="First integer")
    b: int = Field(..., description="Second integer")

@tool(args_schema=AddInput)
def add(a: int, b: int) -> int:
    return a + b

这里的重点是 args_schema=AddInput
也就是说,把工具参数结构放进 Pydantic 类里声明。


方式二:Annotated
from typing_extensions import Annotated
from langchain_core.tools import tool

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

这相当于把参数描述直接贴在类型提示里。
对一些简单工具来说,这种写法会非常干净。


5.6 StructuredTool:更显式、更可配置的工具创建方式

除了 @tool,LangChain 还提供了 StructuredTool.from_function() 这种更显式的方式。

from langchain_core.tools import StructuredTool

def multiply(a: int, b: int) -> int:
    """Multiply two numbers."""
    return a * b

calculator_tool = StructuredTool.from_function(func=multiply)
print(calculator_tool.invoke({"a": 2, "b": 3}))

它适合在你想更明确地配置:

  • 工具名
  • 工具描述
  • 参数 Schema
  • 响应格式

时使用。


5.7 contentartifact:工具返回值也可以分层

有时候,我们希望工具既返回给模型一个简洁的文本结果,又保留一份更完整的原始结构化数据,方便后续日志、分析或链内其他组件继续使用。

这时候可以把工具响应拆成两层:

  • content:给模型看的简洁内容
  • artifact:保留下来的原始结构化数据

例如:

from typing import List, Tuple
from pydantic import BaseModel, Field
from langchain_core.tools import StructuredTool

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

def multiply(a: int, b: int) -> Tuple[str, List[int]]:
    nums = [a, b]
    content = f"{nums}相乘的结果是{a * b}"
    return content, nums

calculator_tool = StructuredTool.from_function(
    func=multiply,
    name="Calculator",
    description="两数相乘",
    args_schema=CalculatorInput,
    response_format="content_and_artifact",
)

这里真正重要的认知是:

模型最适合消费的是简洁文本,链里的其他组件有时更需要原始结构化数据。

所以 artifact 并不是给模型直接看的,而更像是给后续系统内部步骤保留的一份“原料”。


六、绑定工具:让聊天模型知道自己可以调用什么

工具定义好了,还不够。
模型还不知道你手里有什么工具。

这时候就需要用聊天模型的 .bind_tools() 把工具绑定上去。


6.1 最基本的绑定方式

from langchain_openai import ChatOpenAI

model = ChatOpenAI(model="gpt-4o-mini")
tools = [add, multiply]
model_with_tools = model.bind_tools(tools)

这里返回的不是“立即执行结果”,而是一个新的 Runnable 实例
换句话说,绑定工具这件事,本质上还是在生成一个可执行组件。


6.2 bind_tools() 里最值得关注的几个参数

tool_choice

控制模型是否、以及如何使用工具。

常见取值可以这么理解:

  • "auto":模型自动决定要不要用工具
  • "none":不要用工具
  • "any" / "required" / True:至少强制调用一个工具
  • 指定工具名:强制调用某个具体工具
strict

控制模型输出是否要严格匹配工具定义的 Schema。

parallel_tool_calls

是否允许并行工具调用。

这在后面需要一个请求里同时调用多个工具时会很有意义。


6.3 绑定工具后,第一次调用模型会发生什么

来看一个最经典的示例:

from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from typing_extensions import Annotated

model = ChatOpenAI(model="gpt-4o-mini")

@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

model_with_tools = model.bind_tools([add, multiply])

result = model_with_tools.invoke("9乘6等于多少?")
print(result)

这时候返回值依然是 AIMessage
但注意,它不会直接把答案“54”写出来,而是会在消息里告诉你:

我要调用 multiply 这个工具,参数是 a=9, b=6

也就是说,模型此时返回的是工具调用意图,而不是最终答案。


6.4 模型并不总会调用工具

这一点特别重要。
工具不是“绑定了就一定会用”。

如果输入和工具根本没关系,比如:

result = model_with_tools.invoke("hello world!")
print(result)

模型很可能会直接普通回答,而不是硬调工具。

所以工具调用的基本原则是:

模型会根据输入与工具的相关性,决定是否要调用工具。


6.5 如果想强制模型调用工具怎么办

可以在绑定时加上:

model_with_tools = model.bind_tools([add, multiply], tool_choice="any")

这表示:至少调用一个工具

即使输入是一个并不适合工具的问题,模型也会想办法凑一个工具调用出来。

这在某些测试场景下很有用,但真正写业务时,还是更常见 "auto" 这种自然决策模式。


6.6 工具调用信息在哪里看:tool_calls 属性

AIMessage 触发了工具调用时,结果对象里会多出一个非常关键的字段:

print(result.tool_calls)

例如它可能长这样:

[
    {
        "name": "multiply",
        "args": {"a": 9, "b": 6},
        "id": "call_xxx",
        "type": "tool_call"
    }
]

这说明模型已经把最关键的信息都准备好了:

  • 调哪个工具
  • 参数是什么
  • 调用 ID 是什么

而下一步,就是你真的去执行这个工具。


七、工具调用的完整闭环:为什么通常要调两次模型

这一节是整篇里最关键的地方之一。
因为很多人第一次接触工具调用时,只看到了:

model_with_tools.invoke(...)

然后就以为工具流程结束了。

其实这只是第一阶段。


7.1 第一次调用模型,只是在“发起工具调用”

第一次调用模型时,模型做的事情其实是:

  1. 读懂用户问题
  2. 判断是否需要工具
  3. 决定调用哪个工具
  4. 生成工具调用参数
  5. 返回一个包含工具调用信息的 AIMessage

也就是说,这一步只是告诉系统:

“我建议你调用这个工具,并且参数应该这样传。”

它还没有替你执行工具,也还没把最终自然语言答案生成出来。


7.2 为什么一定要有 ToolMessage

LangChain 的聊天模型天然是吃“消息列表”的。
所以当工具执行完成之后,你不能只是自己在代码里偷偷拿到结果完事,而是要把工具结果也包装成消息,再送回模型。

这类消息,就是:

ToolMessage

它的意义非常直接:

告诉模型:你刚刚要求调用的工具,我已经替你执行完了,这是返回结果。

只有这样,模型才有足够上下文去生成最终给用户看的完整回答。


7.3 一个完整闭环示例

from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langchain_core.tools import tool
from typing_extensions import Annotated

model = ChatOpenAI(model="gpt-4o-mini")

@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)

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()]
    tool_msg = selected_tool.invoke(tool_call)
    messages.append(tool_msg)

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

这个流程里,实际上发生了两次模型调用:

第一次调用

发送的是:

  • HumanMessage

返回的是:

  • tool_callsAIMessage
中间步骤

系统根据 tool_calls

  • 执行对应工具
  • 得到 ToolMessage
第二次调用

发送的是:

  • HumanMessage
  • AIMessage
  • ToolMessage

最终返回的才是:

  • 带自然语言答案的 AIMessage

例如:

9乘6等于54,5加3等于8。

这就是工具调用真正的完整闭环。


7.4 为什么这个过程看起来有点麻烦

只要任务开始涉及“决策 → 调工具 → 取结果 → 再推理”,你就已经进入了 Agent 的世界。

这也是为什么后面 LangGraph 会变得特别重要。
因为它就是在解决“这个流程怎样组织得更自然、更稳定”。

但在这一篇里,先把手动版流程看懂,反而非常必要。
因为只有这样,后面学 Agent 时你才知道它到底替你自动化了什么。


7.5 LangChain 也有现成工具:以 TavilySearch 为例

工具不一定都要自己手搓。
LangChain 里已经集成了很多现成工具,像搜索、数据库、网页浏览器等都能找到。

比较典型的一个例子,就是 TavilySearch

from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langchain_tavily import TavilySearch

model = ChatOpenAI(model="gpt-4o-mini")
tool = TavilySearch(max_results=4)

model_with_tools = model.bind_tools([tool])

messages = [HumanMessage("中国西安今天的天气怎么样?")]
ai_msg = model_with_tools.invoke(messages)
messages.append(ai_msg)

for tool_call in ai_msg.tool_calls:
    tool_msg = tool.invoke(tool_call)
    messages.append(tool_msg)

result = model_with_tools.invoke(messages)
print(result.content)

这个示例的价值不在于“天气”本身,而在于它非常清楚地说明了一件事:

一旦有了工具调用,模型就不再只是生成文本,而是开始变成“会协调外部能力的接口层”。


八、结构化输出:从“字符串”走向“对象”

如果说工具调用解决的是“模型怎么接触外部世界”,
那结构化输出解决的就是另一个很现实的问题:

模型输出的自然语言,对人很友好,但对程序不友好。

比如模型返回一段新闻摘要,我们想从里面提取:

  • 公司名
  • 时间
  • 核心事件
  • 股价变化

如果只拿到一段字符串,后面还得自己写解析逻辑。
这时候结构化输出就变得极其重要。


8.1 为什么结构化输出这么重要

它本质上是在做一件事:

把“字符串输出”升级成“符合指定结构的对象输出”

这样一来,模型不再只是给你一段人类可读文本,而是直接给你:

  • Pydantic 对象
  • TypedDict 对应的字典
  • JSON Schema 对应的字典

这一步一旦打通,模型和程序之间的衔接就顺畅很多。


8.2 with_structured_output() 的基本用法

结构化输出最核心的方法就是:

model.with_structured_output(schema)

它的大致流程可以理解为三步:

  1. 定义输出结构
  2. 把这个结构绑定到模型上
  3. 用新的 Runnable 去调用模型

伪代码就是:

schema = {"foo": "bar"}
structured_model = model.with_structured_output(schema)
result = structured_model.invoke(user_input)

重点在于:
这里返回的结果已经不再是普通字符串,也不一定是原始 AIMessage,而是与你定义的 schema 对应的对象。


8.3 返回 Pydantic 对象:最常用、最顺手

这是最适合工程里直接上手的一种写法。

from langchain_openai import ChatOpenAI
from typing import Optional
from pydantic import BaseModel, Field

model = ChatOpenAI(model="gpt-4o-mini")

class Joke(BaseModel):
    """给用户讲一个笑话。"""
    setup: str = Field(description="这个笑话的开头")
    punchline: str = Field(description="这个笑话的妙语")
    rating: Optional[int] = Field(default=None, description="从1到10分,给这个笑话评分")

structured_model = model.with_structured_output(Joke)
result = structured_model.invoke("给我讲一个关于唱歌的笑话")
print(result)

返回结果不是字符串,而是一个 Pydantic 对象,比如:

setup='为什么歌手总是喜欢在洗手间里唱歌?'
punchline='因为那里有很好的回音和灵感!'
rating=7

这个能力特别有用,因为它不只是“格式整齐”,而是直接变成了程序可消费的对象。


8.4 结构化输出也支持嵌套对象

比如:

from typing import List, Optional
from pydantic import BaseModel, Field

class Joke(BaseModel):
    setup: str = Field(description="这个笑话的开头")
    punchline: str = Field(description="这个笑话的妙语")
    rating: Optional[int] = Field(default=None, description="评分")

class Data(BaseModel):
    jokes: List[Joke]

这样一来,模型就可以一次性返回多条结构化记录,而不是散乱的自然语言段落。

这一步非常重要,因为它说明:

结构化输出不是只能做平面字段,它完全可以承载更复杂的数据形态。


8.5 返回 TypedDict:更轻量的结构化字典

如果你不想返回 Pydantic 对象,也可以选择 TypedDict

from typing import Optional
from typing_extensions import Annotated, TypedDict

class Joke(TypedDict):
    """给用户讲一个笑话。"""
    setup: Annotated[str, ..., "这个笑话的开头"]
    punchline: Annotated[str, ..., "这个笑话的妙语"]
    rating: Annotated[Optional[int], None, "从1到10分,给这个笑话评分"]

这类写法的优势在于:

  • 保持字典形态
  • 结构依旧明确
  • 类型信息更清晰

对于有些更偏数据处理的代码来说,这会比 Pydantic 更轻量。


8.6 include_raw=True:把原始响应也一起保留下来

有时候我们不只想要最终解析结果,还想看:

  • 模型原始返回的 AIMessage
  • 解析后的结构化结果
  • 解析过程中有没有错误

这时候可以用:

structured_model = model.with_structured_output(Joke, include_raw=True)

返回结果会带三部分:

  • raw
  • parsed
  • parsing_error

这在调试结构化输出时特别有帮助。
因为你能一眼分清楚,到底是模型没按要求输出,还是解析阶段出了问题。


8.7 返回 JSON:直接基于 JSON Schema

除了 Pydantic 和 TypedDict,也可以直接定义 JSON Schema。

json_schema = {
    "title": "joke",
    "description": "给用户讲一个笑话。",
    "type": "object",
    "properties": {
        "setup": {"type": "string", "description": "这个笑话的开头"},
        "punchline": {"type": "string", "description": "这个笑话的妙语"},
        "rating": {"type": "integer", "description": "评分", "default": None},
    },
    "required": ["setup", "punchline"],
}

再绑定:

structured_model = model.with_structured_output(json_schema)

这样最后得到的就是一个标准字典。

如果你的后端原本就 heavily 依赖 JSON Schema,这种方式会特别顺手。


8.8 联合类型输出:让模型根据语义选格式

有些时候,我们并不总希望模型只返回同一种结构。

比如:

  • 如果用户让它讲笑话,就返回 Joke
  • 如果用户只是普通打招呼,就返回 ConversationalResponse

这时候可以定义联合类型:

from typing import Union, Optional
from pydantic import BaseModel, Field

class Joke(BaseModel):
    setup: str = Field(description="笑话开头")
    punchline: str = Field(description="笑话妙语")
    rating: Optional[int] = Field(default=None, description="评分")

class ConversationalResponse(BaseModel):
    response: str = Field(description="对用户查询的会话响应")

class FinalResponse(BaseModel):
    final_output: Union[Joke, ConversationalResponse]

这个能力说明了一件事:

结构化输出不只是“格式固定”,还可以随着任务语义切换不同结构。

一旦进入复杂场景,这种设计会非常灵活。


九、结构化输出最实用的一个方向:信息提取

所有结构化输出里,最典型、最立刻能用的一个场景,就是:

把自然语言中的信息抽成结构化对象。


9.1 一个典型信息提取例子

from langchain_openai import ChatOpenAI
from typing import Optional
from pydantic import BaseModel, Field
from langchain_core.messages import HumanMessage, SystemMessage

model = ChatOpenAI(model="gpt-4o-mini")

class Person(BaseModel):
    """一个人的信息。"""
    name: Optional[str] = Field(default=None, description="这个人的名字")
    hair_color: Optional[str] = Field(default=None, description="如果知道这个人头发的颜色")
    skin_color: Optional[str] = Field(default=None, description="如果知道这个人的肤色")
    height_in_meters: Optional[str] = Field(default=None, description="以米为单位的高度")

structured_model = model.with_structured_output(schema=Person)

messages = [
    SystemMessage(content="你是一个提取信息的专家,只从文本中提取相关信息。如果你不知道要提取的属性的值,属性值返回null"),
    HumanMessage(content="史密斯身高6英尺,金发。")
]

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

这时候模型返回的就不是一段解释性文字,而是类似:

name='史密斯' hair_color='金发' skin_color=None height_in_meters='1.83'

这比自己写正则表达式去抠字段更方便与稳定


9.2 为什么字段最好尽量写清楚描述

每个字段如果有明确的 description,模型会更清楚这个字段到底该怎么提取。

尤其是:

  • 单位换算
  • 不确定字段可否缺失
  • 某些容易歧义的属性名

这些地方,字段描述写得好不好,会直接影响提取结果质量。


十、结构化输出和工具一起用时,为什么会变得麻烦

这一节非常关键,因为它很容易踩坑。

很多人会想当然地以为:

model.bind_tools(...).with_structured_output(...)

一写完,模型就会:

  1. 自动决定调工具
  2. 自动执行工具
  3. 自动把工具结果整合成结构化输出

但实际并不是这样。


10.1 只绑定工具 + 结构化输出,并不会自动完成工具闭环

比如你定义:

  • 一个 web_search 工具
  • 一个 SearchResult 结构化输出对象

然后写:

model.bind_tools([web_search]).with_structured_output(SearchResult)

这并不意味着工具一定会真的被执行。
模型也可能直接自己生成结构化结果,甚至绕过工具。

所以这里最重要的结论是:

with_structured_output() 只是让模型知道输出应该长什么样,不负责替你自动完成工具调用闭环。


10.2 想让“工具 + 结构化输出”真正工作,仍然要手动拆步骤

正确思路通常是:

  1. 先绑定工具
  2. 先让模型返回工具调用意图
  3. 手动执行工具,得到 ToolMessage
  4. 把整个消息列表重新送给“带结构化输出要求”的模型
  5. 再拿最终结构化结果

像这样:

from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage

model = ChatOpenAI(model="gpt-4o-mini")

class SearchResult(BaseModel):
    query: str = Field(description="搜索查询")
    findings: str = Field(description="调查结果摘要")

@tool
def web_search(query: str) -> str:
    """在网上搜索信息。
    Args:
        query: 搜索查询
    """
    return "西安今天多云转小雨,气温18-23度,东南风2级,空气质量良好"

model_with_search = model.bind_tools([web_search])

messages = [HumanMessage("搜索当前最新的西安的天气")]
ai_msg = model_with_search.invoke(messages)
messages.append(ai_msg)

for tool_call in ai_msg.tool_calls:
    tool_msg = web_search.invoke(tool_call)
    messages.append(tool_msg)

structured_search_model = model_with_search.with_structured_output(SearchResult)
result = structured_search_model.invoke(messages)
print(result)

这才更符合预期。


10.3 这个麻烦,其实已经在暗示 Agent 的必要性

你会发现,这里虽然代码能跑,但写起来确实有点别扭。
因为你实际上是在手动维护一个状态机:

  • 先问模型
  • 看它要不要工具
  • 调工具
  • 补消息
  • 再问模型
  • 还要求最后输出结构化格式

这已经非常接近一个小型 Agent 了。
所以这里最重要的不是“怎么把这段代码记下来”,而是要意识到:

当任务同时涉及“推理 + 工具 + 结构化”,手动拼流程的复杂度会迅速上升。

后面进入 LangGraph / Agent 时,就是为了解决这个问题


十一、流式传输:为什么它对 LLM 应用体验至关重要

到这里,模型已经能:

  • 聊天
  • 调工具
  • 返回结构化结果

但如果用户一提问,界面就一直转圈,然后十几秒之后突然啪地一下把整段文本全吐出来,体验还是很差。

这就是为什么流式传输会变得非常重要。


11.1 非流式调用的问题到底在哪

最简单的非流式调用就是:

model.invoke("讲一个1000字的笑话")

这种方式的问题不在于“不能返回”,而在于:

用户必须等待完整结果全部生成完,才能看到第一点内容。

如果模型思考得久、输出又长,等待感会非常明显。


11.2 stream():同步流式输出

LangChain 的聊天模型本身就支持流式返回。

from langchain_openai import ChatOpenAI

model = ChatOpenAI(model="gpt-4o-mini")

chunks = []
for chunk in model.stream("讲一个50字的笑话"):
    chunks.append(chunk)
    print(chunk.content, end="|", flush=True)

这时候输出不是一次性整段回来,而是一个一个消息块往外冒。

这里最关键的是:

流式返回的不是完整 AIMessage,而是 AIMessageChunk

也就是“消息块”。


11.3 AIMessageChunk 说明了什么

AIMessageChunk 可以理解成:

完整 AI 消息的一小部分

比如有的块里只是一个字,有的块里是几个字。
这些块是可以继续拼起来的。

例如:

print(chunks[0] + chunks[1] + chunks[2])

这说明 LangChain 不是简单地“把字符串切开返回”,而是把流式输出也包装成了统一消息抽象的一部分。

这点非常妙,因为它让“流式”和“非流式”之间并没有彻底割裂,而是在同一套消息体系下演化出来的。


11.4 astream():异步流式输出

同步流式已经够用,但在异步系统里,更常见的写法还是 astream()

from langchain_openai import ChatOpenAI
import asyncio

model = ChatOpenAI(model="gpt-4o-mini")

async def async_stream():
    print("=== 异步调用 ===")
    async for chunk in model.astream("讲一个50字的笑话"):
        print(chunk.content, end="|", flush=True)

asyncio.run(async_stream())

异步流式的价值在于:

它更适合非阻塞工作流,可以和其他异步任务一起被调度。

例如:

  • Web 服务
  • 多路请求处理
  • 实时 UI 更新
  • 并行 I/O 操作

这些场景里,异步流式会更自然。


11.5 协程和事件循环

同步的思路是:

  1. 先煮水
  2. 水开了再发短信

异步的思路是:

  1. 煮水时遇到等待
  2. 把控制权让出去
  3. 去执行别的任务
  4. 等水开了再回来继续

这里的关键词就是:

  • async def:定义协程
  • await:等待时让出控制权
  • asyncio.run():启动事件循环

对 LangChain 而言,是为了理解:

流式输出天然适合异步系统。


11.6 流式传输不是聊天模型“独有能力”,而是 Runnable 的能力

这一点特别重要。
前面讲 Runnable 时我们提过,stream()astream() 属于 Runnable 标准能力之一。

这意味着:

  • 聊天模型可以流式
  • 链也可以流式
  • 某些解析器也可以流式

但要注意,并不是所有组件都必须支持流式。
比如某些检索器就不适合流式返回。

所以更准确的说法应该是:

流式传输是一类 Runnable 组件可能具备的能力,聊天模型只是其中最典型的一种。


11.7 用 StrOutputParser 继续处理流式结果

这一点特别能说明 LangChain 的组件一致性。

from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser

model = ChatOpenAI(model="gpt-4o-mini")
parser = StrOutputParser()

chain = model | parser

for chunk in chain.stream("写一段关于爱情的歌词,需要5句话"):
    print(chunk, end="|", flush=True)

这里模型负责产出 AIMessageChunk
StrOutputParser 则把每个消息块中的文本提取出来。

最终你看到的就是更干净的流式文本,而不是消息对象本身。

这也再次印证了一点:

链不是“先全跑完模型,再统一处理解析器”,而是整个链本身也可以参与流式处理。


11.8 自定义流式输出解析器:按句子而不是按字输出

有时候默认流式粒度太细,比如一个字一个字往外冒。
这时你可以在链里插入一个自定义生成器,把输出重新组织成更适合展示的样式。

例如,把内容按句号拆成一句一句输出:

from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
from typing import Iterator, List

model = ChatOpenAI(model="gpt-4o-mini")
parser = StrOutputParser()

def split_into_list(input: Iterator[str]) -> Iterator[List[str]]:
    buffer = ""
    for chunk in input:
        buffer += chunk
        while "。" in buffer:
            stop_index = buffer.index("。")
            yield [buffer[:stop_index].strip()]
            buffer = buffer[stop_index + 1:]
    yield [buffer.strip()]

chain = model | parser | split_into_list

for chunk in chain.stream("写一份关于爱情的歌词,需要5句话,每句话用句号分割"):
    print(chunk, end="|", flush=True)

这说明流式不仅是“开不开”的问题,还可以在链内部继续定制粒度和样式。


十二、流式传输背后的底层机制:SSE 到底做了什么

前面讲的是“怎么用”,现在该讲“为什么能用”。


12.1 SSE:基于 HTTP 的服务器推送机制

SSE 的全称是:

Server-Sent Events

也就是服务器发送事件。

它的核心特点可以简单理解为:

  • 仍然基于 HTTP
  • 客户端发起请求后,连接保持不断开
  • 服务器可以持续往客户端推送数据块
  • 数据是一段一段送过来的,而不是最后一次性完整返回

所以流式传输并不是 LangChain 自己发明了一套新协议,
而是底层模型服务(比如 OpenAI)本身就支持以这种流形式返回结果。


12.2 LangChain 在这里扮演什么角色

LangChain 本身并不规定底层网络协议。
它做的事情更像是:

  1. 调用模型服务提供的流式 API
  2. 接收对方返回的事件块
  3. 把这些原始块转换成 LangChain 自己统一的消息块对象
  4. 再以 stream() / astream() 的形式暴露给你

也就是说,LangChain 的价值不在“创造 SSE”,而在:

把不同 provider 的流式返回,统一收编成一套消息块抽象。


12.3 从 OpenAI 的角度看,一次流式请求大致经历了什么

从原始材料的源码分析可以提炼成下面这条主线:

第一步:请求里显式开启流式

LangChain 在内部请求里会设置:

stream=True

这就是告诉 OpenAI:

这次不要等完整结果生成完再返回,而是请你边生成边推送事件块。

第二步:OpenAI 以 SSE 事件块方式持续返回

这些块里会带着:

  • role
  • content 的增量片段
  • finish_reason
  • 其他元数据
第三步:LangChain 把原始块转换成 AIMessageChunk

也就是把原始 provider 数据,转换成 LangChain 统一消息格式。

第四步:上层 stream() 不断产出这些块

于是我们在 Python 代码里看到的就是:

for chunk in model.stream(...):
    ...

12.4 流式传输的完整链路

一句话就是:

LangChain 通过底层 provider 的流式 API 发起 HTTP 请求,启用 stream=True,接收 SSE 事件流,再把原始事件块转换成统一的 AIMessageChunk,最后以 Runnable 的流式接口暴露出来。

这句话一旦理解了,后面你再看:

  • stream()
  • astream()
  • AIMessageChunk
  • 链式流式处理

这些东西就都不会显得是“黑盒魔法”了。


十三、LangSmith:当链路开始复杂之后,调试就不该靠猜

前面这些能力单独看都不算太复杂。
但只要你一开始同时做:

  • 多次模型调用
  • 工具调用
  • 结构化输出
  • 流式处理
  • 链式组合

整个应用的内部执行过程很快就会变得不透明。

这时候,最需要的就不是“多打一行 print()”,而是更系统化的可观察性工具。
这就是 LangSmith 出场的原因。

LangSmith 与框架⽆关,它可以与 langchain 和 langgraph ⼀起使⽤,也可以不使⽤。LangSmith 是
⼀个⽤于帮助我们构建⽣产级 LLM 应⽤程序的平台,它将密切监控和评估我们的应⽤。
LangSmith 平台地址


13.1 LangSmith 到底是什么

可以把 LangSmith 理解成:

专门为 LLM 应用提供 tracing、调试、评估和观测能力的平台

它的重要之处在于,它不是单纯帮你记个日志,而是把:

  • 每一次调用
  • 每一个步骤
  • 每一层链路
  • 每一次工具执行
  • 每一次解析结果

都组织成可追踪的运行图。

所以当应用开始复杂时,它能极大减轻“我到底哪里出了问题”的痛苦。


13.2 为什么它对 LangChain 特别重要

因为 LangChain 最大的价值之一就是组合。
而组合一多,链路就复杂。

比如你一个请求里可能同时经历:

  1. 聊天模型第一次判断要不要调工具
  2. 触发工具调用
  3. 工具执行完成
  4. 第二次模型调用做结果整合
  5. 再做结构化输出解析

如果没有 trace,这里面任一步出错,你都得靠猜。
而 LangSmith 做的,就是把这条链拆开展示给你看。


13.3 最基本的接入方式:配环境变量

要开启 LangSmith tracing,先配置两个环境变量:

$env:LANGSMITH_TRACING="true"
$env:LANGSMITH_API_KEY="你的 LangSmith API Key"

在这里插入图片描述

如果是持久化写入,也可以用 setx

它最舒服的一点是:
很多情况下,你不需要改业务逻辑,只要把 tracing 开起来,再正常运行代码,就能在平台里看到执行轨迹。


13.4 一个结合工具和结构化输出的示例

from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage

model = ChatOpenAI(model="gpt-4o-mini")

class SearchResult(BaseModel):
    """结构化搜索结果。"""
    query: str = Field(description="搜索查询")
    findings: str = Field(description="调查结果摘要")

@tool
def web_search(query: str) -> str:
    """在网上搜索信息。
    Args:
        query: 搜索查询
    """
    return "西安今天多云转小雨,气温18-23度,东南风2级,空气质量良好"

model_with_search = model.bind_tools([web_search])

messages = [HumanMessage("搜索当前最新的西安的天气")]
ai_msg = model_with_search.invoke(messages)
messages.append(ai_msg)

for tool_call in ai_msg.tool_calls:
    tool_msg = web_search.invoke(tool_call)
    messages.append(tool_msg)

structured_search_model = model_with_search.with_structured_output(SearchResult)
result = structured_search_model.invoke(messages)
print(result)

当 LangSmith tracing 已开启时,这段代码的执行过程就会被平台记录下来。
在这里插入图片描述
在这里插入图片描述


13.5 在 LangSmith 里,你能看到什么

最有价值的通常是这些东西:

1. 整个调用的瀑布流

你能看到每一步谁先执行,谁后执行,以及各自耗时。

2. 每一步的输入与输出

这对调试非常关键。
尤其是你能看清楚:

  • 模型第一次到底输出了什么工具调用
  • 工具到底返回了什么内容
  • 第二次模型调用拿到了哪些消息
  • 最后结构化输出是怎么解析的
3. 链或序列的实际展开

比如 RunnableSequence 里面具体有哪些环节。

4. 各种 token 与元数据

这会帮助你做成本监控和性能分析。


13.6 LangSmith 真正解决的,不只是“看日志”

很多人第一次听到 tracing,会把它想成“更高级的日志系统”。
但其实它的价值更像是:

把 LLM 应用的内部执行过程从黑盒变成可视化流程图。

尤其是对 LangChain / LangGraph 这类强调组合与多步骤处理的框架来说,这几乎不是可选项,而是越往后越刚需的能力。


十四、本篇总结

这一篇真正要建立起来的,不只是几个 API 的用法,而是一整套关于“聊天模型能力”的理解框架。

  1. 引出 Runnable 与 LCEL。
    模型、解析器、链都能统一执行,本质上来自 Runnable 这套统一抽象。

  2. LCEL 不是简单语法糖,而是 LangChain 的声明式组合语言。
    model | parser 背后表达的是组件间标准化的数据流转。

  3. 聊天模型的定义既可以走具体类,也可以走统一工厂。
    ChatOpenAI 更具体,init_chat_model() 更适合抽象和切换,ChatOllama 则把本地模型也纳入了这套统一接口。

  4. 工具调用的本质,是让模型具备与外部世界交互的能力。
    模型先返回工具调用意图,再由系统执行工具,最后把 ToolMessage 送回模型生成最终答案。
    所以一次完整工具调用,通常不是一步,而是一个闭环。

  5. 结构化输出的意义,是把模型输出从“字符串”提升为“对象”。
    这让模型和程序之间的衔接更稳,也让信息提取、对象落库、流程编排变得自然。

  6. 流式传输不是聊天模型的专属特效,而是 Runnable 体系中的标准能力之一。
    stream()astream()AIMessageChunk,再加上 LCEL 的链式流式处理,共同构成了更好的用户体验。

  7. LangSmith 的价值,是让复杂链路不再只能靠猜。
    当你的应用开始涉及多次模型调用、工具、结构化输出与状态流转时,trace 和可观察性会迅速变成刚需。

如果用一句话总结这一篇,那就是:

从这一篇开始,LangChain 不再只是“帮你调模型”的工具包,而开始真正表现出“标准化组件编排框架”的样子。


💬 下一篇预告:接下来进入 核心组件(Components)。也就是说,后面就会正式走进更系统的组件世界:Prompt、Model、Output Parser、Retriever、Memory、Vector Store……到那时候,前面这一篇里埋下的 Runnable、LCEL、消息系统、工具闭环、结构化输出这些概念,就会全部串起来。🚀

Logo

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

更多推荐