01

定位、结论与阅读范围

从执行契约理解框架,沿真实调用链学习,并把可复用部分落实到工程实践。

LangChain 的核心价值:用统一的模型、消息、工具和 Runnable 契约连接 LLM 应用;用 create_agent 把工具调用循环、状态更新和中间件编译成可运行的 LangGraph 图。掌握这些契约,比记住大量封装 API 更有迁移价值。
问题源码给出的答案
它主要解决什么?把供应商 SDK、消息格式、工具 schema、检索接口与运行时配置连接起来,降低应用层的耦合。
Agent 的循环在哪里?libs/langchain_v1/langchain/agents/factory.py 构建节点和条件边;底层图调度与 checkpoint 能力来自独立的 LangGraph 项目。
这里分析哪一套实现?Python monorepo 中当前维护的 langchain 主线;旧版 chains/AgentExecutor 只说明位置与迁移边界。
最值得复用什么?消息与调用 ID、工具参数契约、模型适配边界、请求包装式中间件、有限预算的执行循环、可观察的状态变化。
分析如何验证?直接阅读固定提交的实现和测试;官方文档补充使用语义。工程建议、示例和 Java 映射均明确标为设计建议或教学实现。

先建立三个判断

固定数据流适合 Runnable 组合;由模型决定工具及下一步的流程适合 create_agent;业务步骤、分支和恢复规则需要严格控制时,直接设计 LangGraph 工作流。

“Think”表示一次模型决策阶段。框架可以处理模型公开返回的 reasoning 内容,但不会凭空获取模型内部思维过程。

读取日期:2026-10-04(中国标准时间)。LangChain 源码固定到 57236d55d9ffc8634bda987861e6fce460aff268,提交时间 2026-10-02 19:35:20 UTC。版本号来自该提交的包配置,不以网页上的动态版本替代。

02

仓库结构与版本边界

目录 / 分发包职责本次快照
libs/core
langchain-core
Runnable、Message、ChatModel、Tool、Retriever、Callback 等基础契约。1.6.6
libs/langchain_v1
langchain
当前 Agent 工厂、中间件、结构化输出策略与模型初始化入口。1.4.3
libs/langchain
langchain-classic
保留传统 chains、旧 Agent 与兼容路径;属于 legacy 层。1.0.8
libs/partnersOpenAI、Anthropic、Ollama 等供应商适配包;各包独立版本与依赖。按包维护
libs/text-splitters文档切分策略,与向量数据库和 Agent 解耦。独立分发
libs/standard-tests让不同模型适配实现满足共同的接口与行为测试。共享测试契约
libs/model-profiles生成和补充模型能力 profile。能力元数据

最容易读错的地方

仓库默认分支是 master。目录名 libs/langchain 不能直接等同于当前安装的 langchain 包:需要先看每个 pyproject.toml 的 name。

当前主包要求 Python ≥3.10,<4,依赖 langchain-core≥1.6.3,<2、langgraph≥1.2.11,<1.3 和 Pydantic 2。依赖范围代表兼容约束,不代表所有包必须使用相同版本号。

生态项目边界
LangChain统一组件接口与可配置 Agent harness。
LangGraph状态图调度、checkpoint、interrupt、跨节点状态合并等底层执行能力,位于另一仓库。
LangSmith追踪、评估与调试平台;不会替应用自动定义质量标准。
Deep Agents建立在 LangChain 之上的更高层能力;规划、文件系统和 subagent 等不应当被当作当前基础工厂自动具备的全部业务行为。
LangChain.js独立的 JS/TS 项目,不属于此次 Python 源码分析范围。

配置依据 主包配置 · Core 配置 · Classic 配置 · 仓库结构说明

03

架构:契约、适配与执行

业务应用:问答 / 审阅 / 推荐 / 自动化
langchain:create_agent / middleware / response_format
LangGraph:StateGraph / ToolNode / Runtime / Checkpointer
langchain-core:Runnable / Message / Model / Tool / Retriever
Partner packages:供应商请求转换 / SDK / 流事件解析
外部模型服务、向量数据库、业务系统

这张分层图用于定位职责,不表示所有请求都必须经过 Agent:一个 prompt | model | parser 链可直接使用 Core 和适配包。Agent 工厂依赖 Core 的数据契约,再把循环交给 LangGraph 执行。

边界进入的数据离开的数据应当拥有的责任
模型接口消息 / PromptValueAIMessage / chunk统一输入输出,转换供应商差异。
工具接口名称 + 参数 + 调用 IDToolMessage / Command校验输入、执行动作、保留结果关联。
图执行AgentState + runtime context状态更新 + 条件路由调度节点、合并结果、终止与恢复。
应用层身份、任务、权限、业务策略可交付结果权限、预算、幂等、质量验收与用户体验。
设计判断:模型接口统一,并不意味着供应商能力完全相同;执行图统一,也不意味着工具副作用自动满足“恰好一次”。这些语义需要在应用层继续定义。

源码定位 create_agent · BaseChatModel · BaseTool

04

Runnable 与 LCEL:组合的基础

Runnable[Input, Output] 把可执行组件统一成“输入 → 输出”契约。提示词、模型、解析器、检索器和工具都可以参与组合。LCEL 是这组组合能力的表达方式,其中 | 构造 RunnableSequence,字典等并行映射可转换为 RunnableParallel。

API语义需要注意
invoke / ainvoke执行一个输入,返回完整结果。基础 ainvoke 可在线程池里调用同步实现;异步方法名不能证明内部是原生异步 I/O。
batch / abatch处理多个独立输入。默认 batch 使用线程池;并非供应商的一次原生批量请求。
stream / astream逐步产生输出。默认实现可以只产生一个完整结果;流式能力由具体组件实现决定。
transform / atransform把输入 chunk 流映射成输出 chunk 流。若组件先收集全部输入,链路会在该处产生流式阻塞。
bind / with_config绑定调用参数 / 运行配置。model 参数与 config 含义不同;temperature 和追踪标签不要混用。
python
from langchain_core.runnables import RunnableLambda, RunnableParallel

add_one = RunnableLambda(lambda x: x + 1)
times_two = RunnableLambda(lambda x: x * 2)
chain = add_one | times_two

assert chain.invoke(3) == 8
assert chain.batch([1, 2]) == [4, 6]

fanout = RunnableParallel(original=lambda x: x, doubled=times_two)
assert fanout.invoke(3) == {"original": 3, "doubled": 6}

为什么 stream() 仍可能很晚才开始输出?

RunnableSequence 通过组件的 transform 保留流式传播。如果中间插入一个收齐全部内容才解析的函数,后续组件必须等待它完成。排查首字延迟时,要同时检查模型启动时间与链路中的阻塞组件。

RunnableParallel 把同一输入分发给多个分支;batch 把多个输入发给同一组件。这是两个不同的并行维度。

源码定位 Runnable · RunnableSequence · RunnableParallel · coerce_to_runnable

05

消息模型:Agent 的执行账本

消息既承载对话内容,也记录工具调用与观察结果。把所有消息压成一个字符串,会丢失角色、工具调用 ID、多模态块和供应商元数据。

对象关键内容执行意义
SystemMessage系统约束、角色、策略。工厂在模型调用时前置 system message。
HumanMessage用户任务及输入内容。作为一次调用的任务入口。
AIMessagecontent、tool_calls、invalid_tool_calls、usage metadata。代表模型答复或待执行动作。
ToolMessagetool_call_id、name、status、content / artifact。把观察结果关联到某次动作。
AIMessageChunk文本、工具参数等增量内容。需要按消息和工具 index/ID 聚合,再得到完整消息。
RemoveMessage被删除消息的 ID。显式表达消息移除,避免把摘要当成简单追加。
python
from langchain_core.messages import AIMessage, ToolMessage

action = AIMessage(
    content="",
    tool_calls=[{
        "name": "lookup_price",
        "args": {"sku": "A100"},
        "id": "call_01",
        "type": "tool_call",
    }],
)
observation = ToolMessage(
    content='{"price": 99.0, "currency": "CNY"}',
    name="lookup_price",
    tool_call_id="call_01",
    status="success",
)

两个 ID 要分清

message.id 标识一条消息;tool_call.id 标识一次工具调用。一个 AIMessage 可以包含多个 tool calls。结果必须通过 tool_call_id 对齐动作,不能仅靠工具名称或返回先后顺序。

artifact 可保存完整的机器处理结果,而 content 放模型需要阅读的摘要。它只是结果分离机制,不自动成为权限隔离或持久化存储。

流式参数不完整时:不要对每个 JSON 字符串片段立即执行工具。等完整工具调用形成并通过 schema 校验后再执行;参数损坏应记录为错误路径。

源码定位 AIMessage · AIMessageChunk · ToolMessage · _patch_invalid_tool_calls

06

ChatModel:把供应商差异隔离在边界

BaseChatModel.invoke() 把字符串、消息列表或 PromptValue 转换为统一输入,进入生成与 callback 链路,最终返回 AIMessage。具体适配器实现 _generate、_stream 及对应异步路径,把供应商 SDK 响应转换为框架消息。

INPUT

标准化输入

字符串成为用户提示;消息保持角色和结构。模型接口接收统一的 LanguageModelInput。

CONFIG

配置与可观察性

规范化 RunnableConfig,建立 callbacks / tags / metadata,进入缓存、限流与生成逻辑。

ADAPTER

转换到供应商协议

适配包构造 API payload,传递工具 schema、response_format 和模型设置,调用 SDK。

OUTPUT

重建标准消息

返回 AIMessage 或 AIMessageChunk,保留 token usage、tool calls 与供应商相关元数据。

能力接口角色兼容边界
init_chat_model根据显式供应商或模型标识初始化对应包;也支持可配置模型。需要安装对应适配包,不能把任意模型名字当作所有供应商都支持。
bind_tools把工具定义绑定到模型请求。只告知模型有哪些工具;业务工具仍由客户端运行时执行。
with_structured_output构造面向 schema 的模型输出链。能力取决于适配器;与 Agent 层的 response_format 不是同一个控制层。
OpenAI-compatible endpoint可复用部分请求格式和适配代码。需验证 tools、流式、错误类型和结构化输出;相同 HTTP 形状不足以证明行为等价。

OpenAI 适配器有消息转换、payload 构造、流 delta 转换等明确职责。应用逻辑应尽量使用标准字段;不得假定每个供应商都会填充完全相同的 response_metadata 或 reasoning 块。

源码定位 BaseChatModel.invoke · BaseChatModel._generate · init_chat_model · _convert_message_to_dict · BaseChatOpenAI._get_request_payload

07

Tool:参数契约与真实动作

@tool 将有类型签名和说明的函数转换为工具;BaseTool 提供名称、描述、参数 schema、调用、结果包装与错误配置。模型负责提出动作,运行时负责验证并执行动作。

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

class PriceArgs(BaseModel):
    sku: str = Field(min_length=1, description="商品 SKU")
    quantity: int = Field(ge=1, le=100, description="采购数量")

@tool(args_schema=PriceArgs)
def lookup_price(sku: str, quantity: int) -> str:
    """查询商品报价,返回单价和采购总价;此示例使用固定数据。"""
    prices = {"A100": 99.0, "B200": 129.0}
    if sku not in prices:
        return "未找到该 SKU;请检查商品编号。"
    unit_price = prices[sku]
    return f"SKU={sku}; 单价={unit_price}; 总价={unit_price * quantity}"

print(lookup_price.invoke({"sku": "A100", "quantity": 2}))
环节机制为什么需要
暴露给模型tool_call_schema 排除注入参数。用户身份、runtime、调用 ID 等由服务端提供,避免模型随意填写。
调用前校验_parse_input 与 schema 校验。限制类型、范围、必填字段,避免参数错误进入业务执行。
执行函数同步 run / 异步 arun。统一 callbacks、错误与输出协议。
结果包装携带完整 ToolCall 输入时,可形成 ToolMessage;content_and_artifact 支持二元结果。仅用普通 dict 调用工具与在 Agent 中带调用 ID 执行,返回语义有差别。
运行时注入ToolNode 注入 state、store、ToolRuntime。工具可读取可信 context;依然要执行应用权限检查。

工具说明也属于接口契约

清楚写明“何时用、输入代表什么、输出是什么、是否有副作用”。一个名为 execute、接收任意 JSON 的万能工具,会把大量验证和路由责任交回模型。

查询类工具便于并发;写入类工具需要幂等键、访问控制和冲突处理。工具 schema 能限制参数形状,但不能判定一次采购提交是否获得真实业务授权。

源码定位 tool · BaseTool.tool_call_schema · BaseTool._parse_input · BaseTool.run · ToolRuntime

08

create_agent:从配置到执行图

工厂在创建时规范化模型、system prompt、工具、响应策略与中间件;合并状态 schema;注册 model、tools 和 hook 节点;连接条件边;最后 compile() 并返回可调用的图。循环发生在运行阶段,构图发生在创建阶段。

Agent 根据模型输出决定执行工具、继续模型调用或结束输入 AgentState模型阶段 + middleware产生 AIMessage条件路由工具 / 结果 / jumpSend → ToolNode校验 / 执行 / ToolMessageafter_agent → END待执行工具观察结果进入下一轮满足终止条件重试
工具返回一般回到模型;结构化输出成功、直接返回策略和中间件跳转可改变路径。示意图省略各独立 hook 节点。
BUILD 01

规范化配置

字符串模型进入 init_chat_model;函数工具由 ToolNode 转成工具实例;dict 工具作为供应商内建工具处理。

BUILD 02

准备状态与中间件

收集中间件工具与 hook;组合 model/tool wrappers;合并 AgentState 和中间件 schema。

BUILD 03

注册节点与条件边

模型阶段产生 ModelResponse;工具节点产生观察结果;条件边根据最新消息和结构化结果选择下一步。

BUILD 04

编译与运行

把 checkpointer、store、interrupt、cache 和 stream transformers 传给 LangGraph。invoke、stream 等调用驱动该图。

运行阶段实际处理输出
模型调用前补齐可关联的历史 invalid tool calls;构造 ModelRequest。messages + tools + response_format + state + runtime。
模型执行wrapper 可替换请求或调用内层;绑定工具与策略,前置 system message,调用模型。AIMessage,以及可选的 structured_response。
状态更新_build_commands 把消息及结果包装为 Command 更新。增量消息;必要时清除上一轮残留 structured_response。
条件路由检查显式 jump、最新 AIMessage、已有工具结果与待执行调用。结束、继续模型,或为各工具生成 Send。
工具执行匹配名称、注入可信 runtime、校验参数并调用工具。带 tool_call_id 的 ToolMessage 或显式 Command。
最小闭环:用户消息 → 模型提出动作 → 工具执行 → 观察结果进入消息历史 → 模型继续决策。若不需要工具,只会保留模型阶段及适用的中间件路径。

源码定位 create_agent · _build_commands · _make_model_to_tools_edge

09

路由、并发与终止条件

当前工厂为每个待执行调用构造 Send("tools", [tool_call])。调用 ID 已有对应 ToolMessage 的动作会被排除;用于结构化输出的“工具”也会从普通业务工具分派中排除。

情况当前路径学习重点
中间件设置 jump_to按允许的跳转目标解析路由。显式控制优先;hook 需声明可跳转目标。
没有最新 AIMessage / 没有 tool_calls进入结束路径。普通 Agent 的基础退出条件。
存在尚未得到结果的业务工具调用为每个调用分派 Send。同轮可并发,结果以调用 ID 关联。
没有待执行工具,且本轮 structured_response 已产生结束。“生成字符串 JSON”不等于验证成功的结构化响应。
工具调用已全部有结果,但仍无最终结构化结果继续模型阶段。可容纳 wrapper 注入的合成 ToolMessage 或结构化修正。
工具节点完成通常回到 loop entry。模型要读取观察结果才能决定后续动作。
同批已执行的客户端工具全部 return_direct=True结束;源码也会考虑 ToolMessage 中的重定向名称。不能简化成“任意一个工具 return_direct 就结束”。

并发能减少等待,也会放大副作用风险

源码具备 fan-out 能力,但依赖关系仍需明确。若第二个动作依赖第一个动作的输出,应让它们在不同决策轮次执行,或把顺序固定到专门工作流。

业务工具若并行写同一资源,需由业务服务保证并发控制;工具 result 的关联正确,不等于数据库更新没有冲突。失败后重放还要考虑请求是否已在远端生效。

终止预算要由应用显式设置

这个提交的工厂默认配置 recursion_limit=9999。它是图执行步数保护,不能当成低成本的业务循环上限。应配置模型调用上限、工具调用上限、总 deadline 与 token/费用预算。

ModelCallLimitMiddleware 通过 before/after model hook 计数;内层 wrapper 或供应商 SDK 的多次请求尝试,不应被直接视为每次都已计入该状态计数。真实请求次数与费用需要单独观测。

源码定位 _make_model_to_tools_edge · _make_tools_to_model_edge · create_agent · ModelCallLimitMiddleware · test_return_direct_graph.py

10

中间件:生命周期与包装调用

扩展点触发范围适合做什么
before_agent / after_agent每次图调用的入口 / 正常完成出口。初始化运行字段、输出整理、任务级检查。
before_model / after_model每轮模型阶段前后。消息裁剪、计数、基于状态的路由。
wrap_model_call包裹实际模型执行。模型路由、缓存、局部重试、请求设置覆盖。
wrap_tool_call包裹每次工具执行。权限、幂等、错误转换、工具策略。
对应 async hook / wrapper异步执行路径。异步 I/O;不要假定同步 wrapper 自动满足所有异步调用方式。

注册顺序决定执行顺序

对 [A, B, C],before hooks 按 A → B → C 执行,after hooks 按 C → B → A 执行。wrapper 的第一个注册项位于最外层:请求 A → B → C → 执行器,结果从内向外返回。

wrapper 可不调用 handler(短路),也可多次调用 handler(重试)。因此“经过一次 model 节点”与“发起一次模型网络请求”并非恒等关系。

python
from langchain.agents.middleware import AgentMiddleware

class StableModelSettings(AgentMiddleware):
    def wrap_model_call(self, request, handler):
        updated = request.override(
            model_settings={**request.model_settings, "temperature": 0}
        )
        return handler(updated)

    async def awrap_model_call(self, request, handler):
        updated = request.override(
            model_settings={**request.model_settings, "temperature": 0}
        )
        return await handler(updated)

教学示例:模型须支持 temperature。request.override() 表达一次请求的覆盖,避免把共享模型实例直接改成某个用户的配置。

设计边界源码事实
动态增加工具若 request.tools 加入未注册的客户端工具,需提前注册到 agent/middleware,或用 wrap_tool_call 承担执行。仅发 schema 给模型不够。
状态与请求hook 返回 state update;wrapper 修改 request 的消息不必然代表持久修改历史。状态写回应采用明确的更新接口。
扩展 ModelResponse当前支持 ExtendedModelResponse 的 Command 状态更新;goto/resume/graph 在 wrap_model_call 返回的 Command 中有显式限制。
结束清理after_agent 是图的正常出口 hook,不是 Python finally。异常、取消、interrupt 时的资源清理要在资源拥有者处保证。

源码定位 AgentMiddleware · ModelRequest.override · _chain_model_call_handlers · _chain_tool_call_wrappers · test_composition.py

11

结构化输出:协议、校验与修正

Agent 的 response_format 定义最终响应 schema。工厂将原始 schema 包成 AutoStrategy,在模型执行时判断使用供应商原生 schema 能力还是 ToolStrategy。自动选择会参考模型 profile 与兼容规则;显式传入策略则保留该选择。

策略实现方式结果与失败路径
ProviderStrategy把 schema 转换为供应商相关的 response_format,绑定到模型请求。无业务 tool calls 时解析并校验输出;成功写 structured_response,校验失败抛结构化输出错误。
ToolStrategy把响应 schema 包装为特殊输出工具;解析其 tool-call args。成功写 structured_response 并补工具确认消息;错误按 handle_errors 配置返回修正消息或抛出。
AutoStrategy保持自动检测意图,按当前模型能力选择。模型被动态切换时仍要检查能力;支持工具调用与原生 structured output 同时使用也有供应商边界。
python
import os
from pydantic import BaseModel, Field
from langchain_openai import ChatOpenAI
from langchain.agents import create_agent
from langchain.agents.structured_output import ToolStrategy
from langchain.agents.middleware import ModelCallLimitMiddleware

class ReviewFinding(BaseModel):
    rule_id: str
    evidence: str = Field(description="与规则判断相关的文档证据")
    conclusion: str

agent = create_agent(
    model=ChatOpenAI(model=os.environ["LC_MODEL_ID"]),
    tools=[],
    response_format=ToolStrategy(ReviewFinding),
    middleware=[ModelCallLimitMiddleware(run_limit=4, exit_behavior="error")],
)
result = agent.invoke({
    "messages": [{"role": "user", "content": "规则 R1 要求签字;片段:签字页为空。"}]
})
finding: ReviewFinding = result["structured_response"]
print(finding.model_dump())

教学示例,需要凭据与支持 tool calling 的模型。 schema 校验保证字段形状;证据是否支持结论仍需业务规则与评估。

为什么不是 JSON parser 就够了?

JsonOutputParser 负责解析模型文本;Agent 的结构化输出策略还负责请求约束、选择机制、状态写入与纠错循环。解析出 JSON 不代表满足 schema,也不代表业务结论正确。

ToolStrategy 收到多个结构化响应或无效字段时,可把错误写为工具消息让模型修正。该路径需要调用预算,否则纠错可能持续消耗资源。ProviderStrategy 的错误不能被默认描述为相同的工具消息重试。

源码定位 ToolStrategy · ProviderStrategy · _supports_provider_strategy · _handle_structured_output_error · JsonOutputParser · test_response_format.py

12

错误处理:让可修复失败进入闭环

失败类型已阅读实现中的行为工程处理建议
工具参数不合法ToolNode 把相应调用校验错误转成 error ToolMessage;可返回给模型修正参数。保留 call ID,给出有限、可操作的反馈。
工具名称未注册ToolNode 返回名称错误的 ToolMessage;工厂也验证动态工具注册边界。避免模型反复选择不存在的工具,检查 schema 与 registry 是否一致。
业务函数内部异常ToolNode 默认处理器处理 ToolInvocationError;其他执行异常默认可继续向外传播。按异常类型显式决定转换、有限重试或终止。
ToolStrategy 校验失败按 handle_errors 决定是否生成反馈消息再调用模型。控制修正次数,并记录错误原因。
ProviderStrategy 校验失败解析校验后抛 StructuredOutputValidationError。在适当调用边界处理;不要假定自动走 ToolStrategy 的修正路径。
模型网络 / 供应商错误模型执行路径可抛异常;适配器对部分错误进行标准化。对可恢复网络错误局部重试;鉴权错误不应盲目循环。
interrupt / GraphBubbleUpToolNode 与 ToolErrorMiddleware 显式传播控制流信号。这是流程暂停/控制协议,不能吞成普通失败结果。
历史 invalid_tool_calls在下一次模型调用前补齐有 ID 且尚无结果的错误消息;测试确保不重复补齐。不能声称模型刚输出 invalid call 就立刻被改写或必然自动重试。
python
from langchain.agents.middleware import ToolErrorMiddleware

def business_error(exc, request):
    if isinstance(exc, TimeoutError):
        return "查询暂时超时;可稍后重试或明确告诉用户当前不可用。"
    return None  # 未选择处理的异常继续传播

error_policy = ToolErrorMiddleware(on_error=business_error)

错误转换与重试分开定义

ToolErrorMiddleware 不执行重试,只把选中的执行异常转换为 ToolMessage(status="error")。返回 None 会继续抛异常。该中间件不会把所有内部异常自动序列化给模型。

重试应尽量包住会失败的最小操作。对整个 Agent 重试可能重复已执行的写入;对单个非幂等工具重试也可能重复提交。先定义业务幂等键和远端状态查询,再考虑自动重试。

源码定位 _default_handle_tool_errors · ToolNode._execute_tool_sync · ToolErrorMiddleware · RunnableRetry · test_invalid_tool_calls.py

13

流式输出:数据增量与执行事件

流式能力分三层:模型 token/chunk、Agent 的节点与状态进展、应用自定义进度。不要把工具参数的增量片段、工具执行事件与最终文本拼成同一条无类型字符串。

接口消费内容使用位置
model.stream()AIMessageChunk 等模型输出增量。简单对话或 LCEL 流链。
agent.stream(..., stream_mode="updates")每一步状态增量。展示模型阶段、工具完成等执行进度。
stream_mode="messages"模型 chunk + metadata。按节点识别 token 来源,避免把摘要模型输出当作最终答复。
stream_mode="custom"应用自行发出的进度信息。长检索、解析或外部任务进度。
stream_events(..., version="v3")run object 上的 typed projections,如 messages、tool_calls、values。当前官方文档推荐给新应用的事件消费方式。
Runnable v1/v2 astream_eventscallback 驱动的事件 dict,含事件名、run ID 等。追踪组件链路;不能与 v3 run object 的返回结构混用。
python
# 复用已创建的 agent;一个完整执行只调用一次 stream_events
run = agent.stream_events(
    {"messages": [{"role": "user", "content": "查询 A100 的报价"}]},
    version="v3",
)

for kind, item in run.interleave("messages", "tool_calls"):
    if kind == "messages":
        for delta in item.text:
            print(delta, end="", flush=True)
    elif kind == "tool_calls":
        print(f"\n执行工具:{item.tool_name}")
        for delta in item.output_deltas:
            print(delta, end="", flush=True)
        print(f"\n工具结果:{item.output}")

final_state = run.output

v3 的 message.tool_calls 展示模型生成的调用参数,run.tool_calls 展示实际执行生命周期。两个投影表达不同阶段。公开 reasoning 投影只有在模型确实返回相关内容时才有数据。

前端与传输层的设计建议

以 run ID、消息 ID、tool-call ID 和节点来源组织 UI;对完成、错误、暂停和取消分别定义状态。工具结果或中间模型文本不一定是最终交付内容。

流结束不应只靠“最后一个文本片段”。要区分图成功完成与连接中断。慢客户端需要有界缓冲或背压;客户端断开后是否继续任务由业务策略决定,不应假定所有外部工具会自动取消。

接口依据 官方 Event Streaming · 传统流模式 · Runnable.astream_events · create_agent

版本细节:该提交注册了 ToolCallTransformer 与 SubagentTransformer,并合并中间件和用户 transformers。当前文档的概览列表未完整展开所有内建项,具体顺序应以固定源码为准。
14

State、Context、Checkpoint 与记忆

对象生命周期 / 用途不能直接等同于
AgentState运行状态;messages 使用 add_messages reducer,另含 structured_response 等字段。简单的 Python list.append。
runtime.context一次调用的依赖与环境,如用户、租户、权限和业务服务。自动持久化的长期记忆。
checkpointer保存 thread 内的图状态,用于后续对话、恢复与暂停继续。跨所有用户共享的知识库。
store通过明确 namespace/key 读写跨 thread 数据。自动识别、提炼、检索全部用户记忆的智能系统。
RunnableWithMessageHistory给一般 Runnable 管理消息历史。当前 create_agent 内部替代 LangGraph checkpoint 的默认实现。

messages 的更新是 reducer,不是无条件追加

add_messages 会标准化输入、为缺失 ID 的消息生成 ID;新 ID 追加,已有 ID 更新;RemoveMessage 表达删除。并发节点的消息结果通过 reducer 汇入状态。

若每次节点返回全部旧消息但又生成新 ID,会造成历史膨胀。教学模型可以先返回消息增量,保持 ID 稳定,再处理编辑、删除和摘要。

python
# 创建 agent 时传入 checkpointer=InMemorySaver()
from langgraph.checkpoint.memory import InMemorySaver

checkpointer = InMemorySaver()
# 应用应确认该 conversation_id 属于当前用户
config = {"configurable": {"thread_id": "conversation_001"}}

# 使用同一个带 checkpointer 的 agent,同一个 thread_id
# 第一轮:agent.invoke({"messages": [...]}, config=config)
# 下一轮只提供新消息,不重新拼接已保存的完整历史
生产边界:InMemorySaver 只适合进程内学习与测试。服务重启、多个实例和长期恢复,需要持久化实现及访问控制。thread_id 是状态定位键,不是身份认证凭证。

上下文压缩与长期记忆要分别设计

SummarizationMiddleware 在上下文预算压力下生成摘要并整理消息;摘要可能丢失细节。关键文档证据、已执行动作与引用 ID 应放在可验证的结构中,不能只依赖摘要文字。

长期记忆需要另外定义:什么值得写入、归属谁、何时过期、如何处理冲突、如何检索和删除。框架提供状态与 store 接口,记忆质量仍由应用策略决定。

源码定位 AgentState · add_messages · create_agent · SummarizationMiddleware · RunnableWithMessageHistory

15

RAG:把知识变成可引用的证据

RAG 把检索到的外部材料放入模型上下文,主要解决知识来源和证据引用。LangChain 的价值在于连接文档、切分、embedding、向量存储和检索器;召回质量、权限与结论可靠性需要另外评估。

INDEX 01

加载与规范化

生成 Document(page_content, metadata),保留来源、页码、租户、文档版本与稳定 chunk ID。

INDEX 02

切分与向量化

TextSplitter 创建片段;Embeddings.embed_documents 编码文本;保存文本、metadata 与向量。

QUERY 01

检索候选

查询进入 Retriever;向量存储实现完成相似度、阈值或 MMR 等搜索。

QUERY 02

构建证据上下文

过滤权限、去重、可选 rerank,按上下文预算构造带来源 ID 的证据块。

ANSWER

生成与校验

模型根据证据答复;检查引用是否存在、是否支持结论,缺少证据时明确返回不足。

接口输入 / 输出关键机制
Document正文 + metadata。来源不会自动随文本拼接而完整保留,应用要明确序列化方式。
RecursiveCharacterTextSplitter文本 → 片段。依序尝试分隔符;长片段递归使用更细分隔符,短片段合并到预算内。
Embeddingsdocuments/query → 向量。区分 embed_documents 与 embed_query;维度与归一化由具体模型决定。
VectorStore存储与搜索接口。框架定义接口,具体索引算法和数据库能力由后端实现。
VectorStoreRetrieverquery string → list[Document]。search_type 支持 similarity、similarity_score_threshold、mmr 等接口路径,后端是否实现仍要确认。

切分参数的实际含义

TextSplitter 的默认 length_function 是 len。因此默认 chunk_size 度量字符长度,不能当作模型 token 数。需要 tokenizer 度量时应显式采用对应切分/长度函数。

chunk_overlap 是片段重叠预算,不保证每个实际边界恰好重复固定数量字符。中文材料可以显式增加句号、逗号等分隔符;采购表格还需保护行列、页码和表头信息。

python
from langchain_core.documents import Document
from langchain_text_splitters import RecursiveCharacterTextSplitter

documents = [Document(
    page_content="采购文件第一章……资格要求……报价规则……",
    metadata={"source": "tender-001", "page": 1, "version": "v1"},
)]
splitter = RecursiveCharacterTextSplitter(
    chunk_size=800,
    chunk_overlap=120,
    separators=["\n\n", "\n", "。", ";", ",", " ", ""],
    add_start_index=True,
)
chunks = splitter.split_documents(documents)
print(chunks[0].metadata)
流程选择模型是否控制检索适用条件
固定 RAG 链应用固定执行检索,再调用模型。每次回答都必须使用证据,流程易测试,调用预算较稳定。
Agentic RAG检索器包装为工具,模型决定是否检索和如何改写查询。需要多轮探索;必须限制次数并检查最终证据。
显式工作流应用固定路由与验收节点,局部允许模型决策。审阅、合规、采购等需要强步骤约束的流程。
工程建议:权限过滤必须发生在可被模型或客户端读取的结果离开检索服务之前。MMR 增加结果多样性,不等于 reranker;reranking 也不会替代访问控制与引用验证。

源码定位 Document · RecursiveCharacterTextSplitter._split_text · TextSplitter._merge_splits · Embeddings · BaseRetriever · VectorStoreRetriever

16

配置传播、追踪与质量评估

RunnableConfig 用于运行维度的控制与追踪。链和模型把 tags、metadata、callbacks 等传给内部执行,形成父子运行关系。模型 API 参数应通过模型设置或 bind 传入;运行配置用于表达任务与执行环境。

字段 / 数据用途设计建议
tags / metadata标记任务类型、模型版本、检索策略等。记录可用于对比的稳定标签;不要随意放完整个人信息或文档正文。
callbacks模型、工具、检索、链的开始/结束/错误事件。把异常发生的组件与业务失败原因关联起来。
run_name / run_id运行标识与追踪关联。统一业务 request ID 和 trace 的映射。
max_concurrency部分 Runnable/执行器的并发控制。不等同于所有供应商调用的全局配额限制。
recursion_limit图执行步数保护。不是模型调用次数,也不是 deadline。
configurable运行时可配置字段,checkpoint thread ID 等。区分可信身份信息与来自用户的参数。
指标层建议观察的内容
性能首字延迟、总时长、每个模型和工具的耗时、检索耗时、排队等待。
可靠性参数校验失败率、工具异常率、重试次数、超时率、取消后仍在运行的任务数。
成本实际模型请求数、输入/输出 token、摘要和重试的额外消耗。
RAG 质量标注集上的 Recall@k、引用正确率、证据支持率、无依据答复率。
Agent 质量任务成功率、错误动作率、重复副作用、预算内完成比例。

可观察性与评估是两件事

trace 解释“发生了什么”,评估回答“做得是否正确”。LangSmith 可承载追踪和评估,但标签、数据集、验收规则、基准与人工抽样仍要由团队定义。

生产日志应优先记录工具名、耗时、结果状态和脱敏摘要。模型输入、工具参数、artifact、checkpoint 和流式内容都可能携带敏感数据,需分别设置保留与访问策略。

源码定位 RunnableConfig · CallbackManager · BaseChatModel.invoke

17

工程边界:预算、恢复与工具权限

问题建议的责任归属落地要点
任务预算应用层 + runtime + 适配层。模型/工具次数、真实 token 消耗、总 deadline、单次网络超时分别控制。
协作取消请求上下文传播到模型与工具。停止新动作;中断支持取消的 I/O;无法取消的远程副作用必须查验状态。
幂等与重放业务工具及下游服务。稳定 operation key、状态查询、去重结果;checkpoint 不自动提供端到端 exactly-once。
授权可信 runtime context + 业务服务。租户过滤、对象级权限、allowlist;prompt 不能替代服务端鉴权。
人工批准HumanInTheLoopMiddleware + checkpointer。暂停后恢复同一任务;审批绑定具体参数,编辑后的动作也需校验。
上下文与状态消息预算 + 摘要策略 + 持久化层。限制历史增长,保护 tool-call 与 result 关联,摘要保留关键证据。
供应商切换适配契约 + 兼容测试。验证 tools/schema/stream/错误语义,不只比较输出文本。

采购审阅场景的组合建议

用显式工作流安排“材料校验 → 证据检索 → 规则判断 → 结构化输出 → 人工确认”。局部 Agent 可用于补充检索与查询,但提交、通知、写入等动作应受明确权限和审批控制。

把证据 ID、页码、文档版本、规则 ID、判断和执行记录作为结构化数据保存。规则判断应可回溯到证据;没有证据的结论不应靠自由文本掩盖。

取舍建议:仅实现 Think → Action → Tool → Observation 的 Java 最小闭环时,可先采用顺序工具执行、有限模型轮次和协作取消。稳定后再增加 checkpoint、并发与跨线程记忆,避免一次承担完整图引擎的复杂度。

源码定位 HumanInTheLoopMiddleware · ModelCallLimitMiddleware · ToolRuntime · RunnableRetry

18

从测试学习:契约与边界

阅读实现后,应回到 tests 观察框架如何定义行为。重点看状态与消息断言、调用顺序、出错后的路径和预算上限,而非只确认“最后生成了文字”。

已检查的用例 / 测试文件明确验证的行为
test_create_agent_does_not_patch_model_output不会在刚返回模型输出时立即改写 invalid tool calls。
test_create_agent_answers_invalid_tool_calls_on_next_turn下一次模型调用前补齐可关联的历史错误结果。
test_create_agent_does_not_duplicate_historical_tool_messages已有工具结果不再重复补齐。
test_composition.pywrapper 注册顺序、内层重试、请求覆盖及状态更新组合。
test_structured_output_retry_exceeds_max_retries结构化输出修正也必须有最大次数。
test_response_format.pyProvider / Tool 策略、schema 与错误路径等结构化契约。
test_return_direct_graph.pyreturn_direct 对构图与路由的影响。
ChatModelUnitTests模型适配器的标准输入输出与能力接口。
bash
# 在已固定版本的仓库中,先准备当前包环境
cd libs/langchain_v1
uv sync --group test

# 优先使用快速测试目标,避免完整测试启动数据库服务
make test_fast TEST_FILE=tests/unit_tests/agents/test_invalid_tool_calls.py

# 继续按同样方式选择具体用例文件
make test_fast TEST_FILE=tests/unit_tests/agents/middleware/core/test_composition.py

该提交的 make test 会启动 Postgres/Redis 服务并涉及 snapshot 更新;test_fast 适合先读和验证局部契约。不要直接把根目录当成统一 Python workspace,包级命令应在对应目录执行。

自行实现 Runtime 时值得保留的用例

  • 模型直接回答:零工具调用,正确结束。
  • 合法工具调用:动作、结果与下一轮消息严格按 ID 对齐。
  • 非法参数、未知工具、选中处理的业务异常:结果进入预期的错误路径。
  • 同批工具结果乱序:关联仍正确,不依赖完成顺序。
  • 预算耗尽、取消、超时:不再发起新动作,终止原因可观察。
  • 重试与恢复:不会静默重复已确认的业务副作用。

验证范围:检查了上述源码与测试定义;没有在本次环境执行 LangChain 完整测试套件或真实供应商调用。示例代码已做语法检查,HTML 的交互与布局另行验收。

19

实践:从离线组合到真实 Agent

建议先验证数据与执行契约,再接入外部模型。下面两个示例分别用于理解 LCEL 的固定数据流与 Agent 的工具循环。

实验 A · 可在离线环境执行的 RAG 数据流

安装 langchain-core 后无需模型凭据。DemoRetriever 返回固定材料,FakeListChatModel 返回固定答案;这个实验验证组合接口与数据流,不验证向量检索效果或模型推理能力。

python
from langchain_core.documents import Document
from langchain_core.retrievers import BaseRetriever
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
from langchain_core.language_models.fake_chat_models import FakeListChatModel

class DemoRetriever(BaseRetriever):
    documents: list[Document]

    def _get_relevant_documents(self, query, *, run_manager):
        return self.documents

retriever = DemoRetriever(documents=[Document(
    page_content="供应商需在报价单上签字。",
    metadata={"source_id": "R1", "page": 3},
)])

def format_docs(docs):
    return "\n".join(
        f"[{d.metadata['source_id']}] 第{d.metadata['page']}页:{d.page_content}"
        for d in docs
    )

prompt = ChatPromptTemplate.from_messages([
    ("system", "只根据证据回答,保留证据 ID。"),
    ("human", "证据:\n{context}\n问题:{question}"),
])
model = FakeListChatModel(responses=["报价单需要签字 [R1]。"])
chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | prompt
    | model
    | StrOutputParser()
)

answer = chain.invoke("报价单是否需要签字?")
assert answer == "报价单需要签字 [R1]。"
print(answer)

实验 B · 有界工具调用 Agent

依赖 langchain、langchain-openai;设置 OPENAI_API_KEY 与账户支持的 LC_MODEL_ID。模型 ID 由环境配置,示例不会绑定随时间失效的固定型号。

python
import os
from langchain_openai import ChatOpenAI
from langchain.agents import create_agent
from langchain.agents.middleware import ModelCallLimitMiddleware
from langchain_core.tools import tool

@tool
def lookup_policy(rule_id: str) -> str:
    """查询采购规则,输入规则 ID,返回带规则 ID 的证据文本。"""
    policies = {"R1": "报价单需签字,依据:[R1]。"}
    return policies.get(rule_id, "未找到该规则,证据不足。")

model = ChatOpenAI(
    model=os.environ["LC_MODEL_ID"],
    timeout=20,
    max_retries=1,
)
agent = create_agent(
    model=model,
    tools=[lookup_policy],
    system_prompt="查询规则后回答,保留证据 ID;无法查询时明确说明。",
    middleware=[ModelCallLimitMiddleware(run_limit=4, exit_behavior="error")],
)
result = agent.invoke(
    {"messages": [{"role": "user", "content": "请查询 R1,说明报价单要求。"}]},
    config={"recursion_limit": 30, "tags": ["policy-demo"]},
)
print(result["messages"][-1].content)
for message in result["messages"]:
    print(type(message).__name__, getattr(message, "tool_call_id", None))
实验观察对应原理
实验 A 的 context 与 question 并行分支RunnableParallel 的相同输入分发。
实验 B 中 AIMessage 与 ToolMessage模型动作和观察结果组成下一轮上下文。
把 rule_id 改为不存在值工具应返回真实的“证据不足”,而非伪造内容。
把模型调用上限降低观察预算结束原因,确认没有额外无限循环。
再加 checkpointer 并复用 thread_id验证只传新消息时历史能否正确恢复。

模型单次 timeout、SDK max_retries、图 recursion_limit 与模型阶段上限表达不同约束。实验 B 未设置整个业务任务的总 deadline,生产实现需要在调用入口补齐。

源码定位 BaseRetriever · ChatPromptTemplate · RunnableParallel · create_agent

20

迁移到 Java:保留语义,控制范围

以下是设计建议,用于自研 Java Agent Runtime。目标是迁移契约与执行语义;没有把 Python 类和完整 LangGraph 图引擎逐一照搬。

LangChain / LangGraph 抽象建议的 Java 抽象必须保留的语义
BaseChatModel / AIMessageModelClient、ModelResponse、StreamEvent。供应商转换隔离;文本、工具调用、结束与错误有类型。
Message 类型AgentMessage 与供应商 DTO 分离。角色、消息 ID、tool_call ID、多模态及元数据明确。
BaseTool / ToolNodeTool + ToolRegistry + ToolExecutor。schema、输入校验、可信 context 注入、结构化结果。
ModelRequest / wrappers不可变 ModelRequest + ModelInterceptor。请求覆盖、顺序、短路与局部重试明确。
AgentState / reducerAgentState + 显式 StateUpdate。增量消息与稳定 ID,不把状态与供应商消息格式混在一起。
条件边AgentLoop / NextAction。模型决策、pending calls、工具结果与终止原因明确。
Runtime.contextRunContext。身份、tenant、deadline、取消令牌等由服务端提供。
Checkpointer / Store后续独立接口。先完成最小闭环,再扩展恢复和跨会话记忆。
java-pseudocode
// Java 最小闭环示意,不是 LangChain 源码逐行翻译
while (budget.allowNextModelCall() && !context.isCancelled()) {
    ModelResponse response = model.generate(
        messageMapper.toModelInput(state.messages()), tools.schemas(), context
    );
    state.apply(response.toStateUpdate());

    List<ToolCall> pending = pendingCalls(response, state);
    if (pending.isEmpty()) {
        return completed(state);
    }

    for (ToolCall call : pending) {
        context.checkDeadlineAndCancellation();
        ToolResult result = executor.executeValidated(call, context);
        state.apply(result.toStateUpdate());
    }
}
return stopped(state, budgetOrCancellationReason(context));

JDK 8 和 JDK 17 的实现取舍

JDK 8 可用普通接口、不可变 POJO、ExecutorService 与 CompletableFuture;JDK 17 可用 record 和 sealed interface 改善类型表达。两套实现的行为契约应一致,不能把语言特性差异变成消息或终止语义差异。

第一阶段建议按序执行工具,固定最大模型轮次,提供选定错误类型的结果化处理与协作取消。并发工具、checkpoint、长期记忆、MCP 和复杂工作流可作为独立阶段接入。

可迁移的核心:把模型决定、动作执行、观察结果、状态变化与终止原因分别建模。稳定的类型边界和测试,能够比大量工具封装更直接地降低 Runtime 复杂度。
21

学习顺序与验收任务

PHASE 01

先读契约,再运行离线组合

阅读 Runnable、Message、BaseChatModel、BaseTool;运行实验 A。验收:能解释 invoke/batch/stream 的差异及 ID 的作用。

PHASE 02

沿一次工具调用读工厂

追踪 create_agent → model_node → _execute_model_sync → _build_commands → 条件边。验收:列出每一步的输入、输出和责任。

PHASE 03

补齐失败和结束路径

阅读 invalid_tool_calls、return_direct 和 structured-output 测试。验收:为每个边界写出状态和路由预期。

PHASE 04

理解扩展顺序与状态恢复

阅读 middleware、add_messages、checkpoint 和 streaming。验收:解释 wrapper 重试次数为何可能不同于模型节点次数。

PHASE 05

完成可评估的业务闭环

接入实际检索与一个只读业务工具,再添加预算、权限、trace 和引用校验。验收:用固定样本解释成功率、证据支持率与成本。

建议的源码阅读路线

  1. Runnable → RunnableSequence → RunnableParallel。
  2. AIMessage → ToolMessage → BaseChatModel.invoke。
  3. @tool → BaseTool._parse_input → ToolNode。
  4. create_agent → 模型节点 → _build_commands → 两个方向的条件边。
  5. AgentMiddleware → wrapper 组合 → 结构化输出策略。
  6. add_messages → retriever / splitter → 对应测试。

用解释能力检查学习结果

为什么绑定工具后,模型不能直接调用 Python 函数?

bind_tools 绑定的是名称、说明和参数 schema;模型返回 tool calls。客户端运行时通过 registry 匹配工具并执行,再把 ToolMessage 送入后续模型上下文。供应商内建工具属于另一种执行边界。

为什么不能把所有工具异常都自动返回给模型?

有的错误是可修复的参数问题,有的是内部程序错误,还有的是 interrupt 等控制流。框架的默认处理和应用策略必须区分这些情况;返回错误消息也应避免泄露内部细节。

为什么不能把 recursion_limit 当作模型调用次数?

图中还可能执行工具和 hook 节点;wrapper/SDK 又可能在一个模型阶段发起多个尝试。因此步数、模型阶段数和真实网络请求数需要分别统计。

为什么有 checkpoint 仍然可能重复业务动作?

持久化图状态和远端业务写入通常不在同一个原子事务里。崩溃或重试可能落在“业务已写入、状态未确认”的窗口,需要业务幂等和状态查询。

为什么 schema 正确仍然不代表审阅结果正确?

schema 校验检查类型与字段约束,无法证明 evidence 真实存在或足以支持 conclusion。引用完整性、规则判断和业务正确性需要独立验收。

什么时候应该选择显式工作流?

当步骤顺序、授权、人工批准、失败恢复和验收边界必须可控时,由应用明确安排图节点;模型决策仅放在需要灵活探索的局部阶段。

22

固定源码与参考资料

所有 LangChain 源码链接固定到 57236d55d9ffc8634bda987861e6fce460aff268。作为底层机制补充的 LangGraph 源码单独固定到 9a0394d88b2211f299dcd69df92db3480c69ee61;它是独立仓库快照,不宣称与本次主包 uv.lock 解析出的分发版本完全相同。

  1. Agent 工厂与路由
    libs/langchain_v1/langchain/agents/factory.py
  2. 中间件类型与请求契约
    libs/langchain_v1/langchain/agents/middleware/types.py
  3. 结构化输出策略
    libs/langchain_v1/langchain/agents/structured_output.py
  4. Runnable 与组合算子
    libs/core/langchain_core/runnables/base.py
  5. 运行配置
    libs/core/langchain_core/runnables/config.py
  6. ChatModel 核心执行
    libs/core/langchain_core/language_models/chat_models.py
  7. AI 消息与增量聚合
    libs/core/langchain_core/messages/ai.py
  8. 工具结果消息
    libs/core/langchain_core/messages/tool.py
  9. 工具输入与执行契约
    libs/core/langchain_core/tools/base.py
  10. 函数到工具的转换
    libs/core/langchain_core/tools/convert.py
  11. OpenAI 供应商适配
    libs/partners/openai/langchain_openai/chat_models/base.py
  12. Retriever 契约
    libs/core/langchain_core/retrievers.py
  13. VectorStore 与检索包装
    libs/core/langchain_core/vectorstores/base.py
  14. 递归切分器
    libs/text-splitters/langchain_text_splitters/character.py
  15. 工具执行错误转换
    libs/langchain_v1/langchain/agents/middleware/tool_error.py
  16. 模型阶段调用上限
    libs/langchain_v1/langchain/agents/middleware/model_call_limit.py
  17. 人工批准中间件
    libs/langchain_v1/langchain/agents/middleware/human_in_the_loop.py
  18. LangGraph:工具节点与 runtime
    libs/prebuilt/langgraph/prebuilt/tool_node.py
  19. LangGraph:消息 reducer
    libs/langgraph/langgraph/graph/message.py
官方文档补充内容
LangChain Overview生态定位与标准模型接口。
Event Streamingv3 typed projections、模型消息与工具执行事件。
Streamingstream_mode 的执行进度、消息与自定义输出。
Short-term Memorycheckpoint、thread 与会话状态使用方式。
Middleware Overview生命周期扩展与包装模型/工具调用。
阅读证据的方式:先看契约定义,再跟调用链,最后用测试确认边界。文中“源码事实”针对固定快照;工程建议、Java 映射与教学示例需要结合实际项目验证。
← 返回内容索引