零基础入门 LangChain 与 LangGraph(四):聊天模型进阶——工具调用、结构化输出、流式传输与 LangSmith
文章目录
-
- 零基础入门 LangChain 与 LangGraph(四):聊天模型进阶——工具调用、结构化输出、流式传输与 LangSmith
- 一、回顾
- 二、Runnable:LangChain 最重要的统一抽象
- 三、LCEL:LangChain 的声明式组合语言
- 四、聊天模型的两种定义方式:具体类与统一工厂
- 五、工具调用:让模型真正具备与外部世界交互的能力
- 六、绑定工具:让聊天模型知道自己可以调用什么
- 七、工具调用的完整闭环:为什么通常要调两次模型
- 八、结构化输出:从“字符串”走向“对象”
- 九、结构化输出最实用的一个方向:信息提取
- 十、结构化输出和工具一起用时,为什么会变得麻烦
- 十一、流式传输:为什么它对 LLM 应用体验至关重要
- 十二、流式传输背后的底层机制:SSE 到底做了什么
- 十三、LangSmith:当链路开始复杂之后,调试就不该靠猜
- 十四、本篇总结
零基础入门 LangChain 与 LangGraph(四):聊天模型进阶——工具调用、结构化输出、流式传输与 LangSmith
💬 开篇说明:上一篇我们已经把环境搭建、基础安装和第一个 LangChain 程序跑通了。这一篇不再停留在“能不能调起模型”,而是继续往前走,真正进入 LangChain 聊天模型最关键的几项能力:工具调用、结构化输出、流式传输,以及 LangSmith 跟踪调试。
👍 这一篇的定位:如果说第三篇解决的是“LangChain 到底怎么启动”,那这一篇解决的就是“LangChain 为什么开始变得像一个框架”。
🚀 这一篇的目标:写完之后,至少要把下面这些事情搞明白:
- 为什么模型、输出解析器、链都能统一用
invoke()- 工具调用到底是怎么完成一次完整闭环的
- 结构化输出为什么能把“字符串”变成“对象”
- 流式传输背后的真实机制是什么
- 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:响应元数据,比如模型名、完成原因、请求 IDusage_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 为什么 model 和 parser 都能 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 Keybase_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 content 和 artifact:工具返回值也可以分层
有时候,我们希望工具既返回给模型一个简洁的文本结果,又保留一份更完整的原始结构化数据,方便后续日志、分析或链内其他组件继续使用。
这时候可以把工具响应拆成两层:
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 第一次调用模型,只是在“发起工具调用”
第一次调用模型时,模型做的事情其实是:
- 读懂用户问题
- 判断是否需要工具
- 决定调用哪个工具
- 生成工具调用参数
- 返回一个包含工具调用信息的
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_calls的AIMessage
中间步骤
系统根据 tool_calls:
- 执行对应工具
- 得到
ToolMessage
第二次调用
发送的是:
HumanMessageAIMessageToolMessage
最终返回的才是:
- 带自然语言答案的
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)
它的大致流程可以理解为三步:
- 定义输出结构
- 把这个结构绑定到模型上
- 用新的 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)
返回结果会带三部分:
rawparsedparsing_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(...)
一写完,模型就会:
- 自动决定调工具
- 自动执行工具
- 自动把工具结果整合成结构化输出
但实际并不是这样。
10.1 只绑定工具 + 结构化输出,并不会自动完成工具闭环
比如你定义:
- 一个
web_search工具 - 一个
SearchResult结构化输出对象
然后写:
model.bind_tools([web_search]).with_structured_output(SearchResult)
这并不意味着工具一定会真的被执行。
模型也可能直接自己生成结构化结果,甚至绕过工具。
所以这里最重要的结论是:
with_structured_output()只是让模型知道输出应该长什么样,不负责替你自动完成工具调用闭环。
10.2 想让“工具 + 结构化输出”真正工作,仍然要手动拆步骤
正确思路通常是:
- 先绑定工具
- 先让模型返回工具调用意图
- 手动执行工具,得到
ToolMessage - 把整个消息列表重新送给“带结构化输出要求”的模型
- 再拿最终结构化结果
像这样:
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 协程和事件循环
同步的思路是:
- 先煮水
- 水开了再发短信
异步的思路是:
- 煮水时遇到等待
- 把控制权让出去
- 去执行别的任务
- 等水开了再回来继续
这里的关键词就是:
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 本身并不规定底层网络协议。
它做的事情更像是:
- 调用模型服务提供的流式 API
- 接收对方返回的事件块
- 把这些原始块转换成 LangChain 自己统一的消息块对象
- 再以
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 最大的价值之一就是组合。
而组合一多,链路就复杂。
比如你一个请求里可能同时经历:
- 聊天模型第一次判断要不要调工具
- 触发工具调用
- 工具执行完成
- 第二次模型调用做结果整合
- 再做结构化输出解析
如果没有 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 的用法,而是一整套关于“聊天模型能力”的理解框架。
-
引出 Runnable 与 LCEL。
模型、解析器、链都能统一执行,本质上来自 Runnable 这套统一抽象。 -
LCEL 不是简单语法糖,而是 LangChain 的声明式组合语言。
model | parser背后表达的是组件间标准化的数据流转。 -
聊天模型的定义既可以走具体类,也可以走统一工厂。
ChatOpenAI更具体,init_chat_model()更适合抽象和切换,ChatOllama则把本地模型也纳入了这套统一接口。 -
工具调用的本质,是让模型具备与外部世界交互的能力。
模型先返回工具调用意图,再由系统执行工具,最后把ToolMessage送回模型生成最终答案。
所以一次完整工具调用,通常不是一步,而是一个闭环。 -
结构化输出的意义,是把模型输出从“字符串”提升为“对象”。
这让模型和程序之间的衔接更稳,也让信息提取、对象落库、流程编排变得自然。 -
流式传输不是聊天模型的专属特效,而是 Runnable 体系中的标准能力之一。
stream()、astream()、AIMessageChunk,再加上 LCEL 的链式流式处理,共同构成了更好的用户体验。 -
LangSmith 的价值,是让复杂链路不再只能靠猜。
当你的应用开始涉及多次模型调用、工具、结构化输出与状态流转时,trace 和可观察性会迅速变成刚需。
如果用一句话总结这一篇,那就是:
从这一篇开始,LangChain 不再只是“帮你调模型”的工具包,而开始真正表现出“标准化组件编排框架”的样子。
💬 下一篇预告:接下来进入 核心组件(Components)。也就是说,后面就会正式走进更系统的组件世界:Prompt、Model、Output Parser、Retriever、Memory、Vector Store……到那时候,前面这一篇里埋下的 Runnable、LCEL、消息系统、工具闭环、结构化输出这些概念,就会全部串起来。🚀
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)