LangChain
源码原理与工程实践
从执行契约到可验证的 Agent 闭环
定位、结论与阅读范围
从执行契约理解框架,沿真实调用链学习,并把可复用部分落实到工程实践。
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。版本号来自该提交的包配置,不以网页上的动态版本替代。
仓库结构与版本边界
| 目录 / 分发包 | 职责 | 本次快照 |
|---|---|---|
libs/corelangchain-core | Runnable、Message、ChatModel、Tool、Retriever、Callback 等基础契约。 | 1.6.6 |
libs/langchain_v1langchain | 当前 Agent 工厂、中间件、结构化输出策略与模型初始化入口。 | 1.4.3 |
libs/langchainlangchain-classic | 保留传统 chains、旧 Agent 与兼容路径;属于 legacy 层。 | 1.0.8 |
libs/partners | OpenAI、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 配置 · 仓库结构说明
架构:契约、适配与执行
这张分层图用于定位职责,不表示所有请求都必须经过 Agent:一个 prompt | model | parser 链可直接使用 Core 和适配包。Agent 工厂依赖 Core 的数据契约,再把循环交给 LangGraph 执行。
| 边界 | 进入的数据 | 离开的数据 | 应当拥有的责任 |
|---|---|---|---|
| 模型接口 | 消息 / PromptValue | AIMessage / chunk | 统一输入输出,转换供应商差异。 |
| 工具接口 | 名称 + 参数 + 调用 ID | ToolMessage / Command | 校验输入、执行动作、保留结果关联。 |
| 图执行 | AgentState + runtime context | 状态更新 + 条件路由 | 调度节点、合并结果、终止与恢复。 |
| 应用层 | 身份、任务、权限、业务策略 | 可交付结果 | 权限、预算、幂等、质量验收与用户体验。 |
源码定位 create_agent · BaseChatModel · BaseTool
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 和追踪标签不要混用。 |
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
消息模型:Agent 的执行账本
消息既承载对话内容,也记录工具调用与观察结果。把所有消息压成一个字符串,会丢失角色、工具调用 ID、多模态块和供应商元数据。
| 对象 | 关键内容 | 执行意义 |
|---|---|---|
SystemMessage | 系统约束、角色、策略。 | 工厂在模型调用时前置 system message。 |
HumanMessage | 用户任务及输入内容。 | 作为一次调用的任务入口。 |
AIMessage | content、tool_calls、invalid_tool_calls、usage metadata。 | 代表模型答复或待执行动作。 |
ToolMessage | tool_call_id、name、status、content / artifact。 | 把观察结果关联到某次动作。 |
AIMessageChunk | 文本、工具参数等增量内容。 | 需要按消息和工具 index/ID 聚合,再得到完整消息。 |
RemoveMessage | 被删除消息的 ID。 | 显式表达消息移除,避免把摘要当成简单追加。 |
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 放模型需要阅读的摘要。它只是结果分离机制,不自动成为权限隔离或持久化存储。
源码定位 AIMessage · AIMessageChunk · ToolMessage · _patch_invalid_tool_calls
ChatModel:把供应商差异隔离在边界
BaseChatModel.invoke() 把字符串、消息列表或 PromptValue 转换为统一输入,进入生成与 callback 链路,最终返回 AIMessage。具体适配器实现 _generate、_stream 及对应异步路径,把供应商 SDK 响应转换为框架消息。
标准化输入
字符串成为用户提示;消息保持角色和结构。模型接口接收统一的 LanguageModelInput。
配置与可观察性
规范化 RunnableConfig,建立 callbacks / tags / metadata,进入缓存、限流与生成逻辑。
转换到供应商协议
适配包构造 API payload,传递工具 schema、response_format 和模型设置,调用 SDK。
重建标准消息
返回 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
Tool:参数契约与真实动作
@tool 将有类型签名和说明的函数转换为工具;BaseTool 提供名称、描述、参数 schema、调用、结果包装与错误配置。模型负责提出动作,运行时负责验证并执行动作。
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
create_agent:从配置到执行图
工厂在创建时规范化模型、system prompt、工具、响应策略与中间件;合并状态 schema;注册 model、tools 和 hook 节点;连接条件边;最后 compile() 并返回可调用的图。循环发生在运行阶段,构图发生在创建阶段。
规范化配置
字符串模型进入 init_chat_model;函数工具由 ToolNode 转成工具实例;dict 工具作为供应商内建工具处理。
准备状态与中间件
收集中间件工具与 hook;组合 model/tool wrappers;合并 AgentState 和中间件 schema。
注册节点与条件边
模型阶段产生 ModelResponse;工具节点产生观察结果;条件边根据最新消息和结构化结果选择下一步。
编译与运行
把 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
路由、并发与终止条件
当前工厂为每个待执行调用构造 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
中间件:生命周期与包装调用
| 扩展点 | 触发范围 | 适合做什么 |
|---|---|---|
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 节点”与“发起一次模型网络请求”并非恒等关系。
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
结构化输出:协议、校验与修正
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 同时使用也有供应商边界。 |
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
错误处理:让可修复失败进入闭环
| 失败类型 | 已阅读实现中的行为 | 工程处理建议 |
|---|---|---|
| 工具参数不合法 | ToolNode 把相应调用校验错误转成 error ToolMessage;可返回给模型修正参数。 | 保留 call ID,给出有限、可操作的反馈。 |
| 工具名称未注册 | ToolNode 返回名称错误的 ToolMessage;工厂也验证动态工具注册边界。 | 避免模型反复选择不存在的工具,检查 schema 与 registry 是否一致。 |
| 业务函数内部异常 | ToolNode 默认处理器处理 ToolInvocationError;其他执行异常默认可继续向外传播。 | 按异常类型显式决定转换、有限重试或终止。 |
| ToolStrategy 校验失败 | 按 handle_errors 决定是否生成反馈消息再调用模型。 | 控制修正次数,并记录错误原因。 |
| ProviderStrategy 校验失败 | 解析校验后抛 StructuredOutputValidationError。 | 在适当调用边界处理;不要假定自动走 ToolStrategy 的修正路径。 |
| 模型网络 / 供应商错误 | 模型执行路径可抛异常;适配器对部分错误进行标准化。 | 对可恢复网络错误局部重试;鉴权错误不应盲目循环。 |
| interrupt / GraphBubbleUp | ToolNode 与 ToolErrorMiddleware 显式传播控制流信号。 | 这是流程暂停/控制协议,不能吞成普通失败结果。 |
| 历史 invalid_tool_calls | 在下一次模型调用前补齐有 ID 且尚无结果的错误消息;测试确保不重复补齐。 | 不能声称模型刚输出 invalid call 就立刻被改写或必然自动重试。 |
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
流式输出:数据增量与执行事件
流式能力分三层:模型 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_events | callback 驱动的事件 dict,含事件名、run ID 等。 | 追踪组件链路;不能与 v3 run object 的返回结构混用。 |
# 复用已创建的 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.outputv3 的 message.tool_calls 展示模型生成的调用参数,run.tool_calls 展示实际执行生命周期。两个投影表达不同阶段。公开 reasoning 投影只有在模型确实返回相关内容时才有数据。
前端与传输层的设计建议
以 run ID、消息 ID、tool-call ID 和节点来源组织 UI;对完成、错误、暂停和取消分别定义状态。工具结果或中间模型文本不一定是最终交付内容。
流结束不应只靠“最后一个文本片段”。要区分图成功完成与连接中断。慢客户端需要有界缓冲或背压;客户端断开后是否继续任务由业务策略决定,不应假定所有外部工具会自动取消。
接口依据 官方 Event Streaming · 传统流模式 · Runnable.astream_events · create_agent
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 稳定,再处理编辑、删除和摘要。
# 创建 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)
# 下一轮只提供新消息,不重新拼接已保存的完整历史上下文压缩与长期记忆要分别设计
SummarizationMiddleware 在上下文预算压力下生成摘要并整理消息;摘要可能丢失细节。关键文档证据、已执行动作与引用 ID 应放在可验证的结构中,不能只依赖摘要文字。
长期记忆需要另外定义:什么值得写入、归属谁、何时过期、如何处理冲突、如何检索和删除。框架提供状态与 store 接口,记忆质量仍由应用策略决定。
源码定位 AgentState · add_messages · create_agent · SummarizationMiddleware · RunnableWithMessageHistory
RAG:把知识变成可引用的证据
RAG 把检索到的外部材料放入模型上下文,主要解决知识来源和证据引用。LangChain 的价值在于连接文档、切分、embedding、向量存储和检索器;召回质量、权限与结论可靠性需要另外评估。
加载与规范化
生成 Document(page_content, metadata),保留来源、页码、租户、文档版本与稳定 chunk ID。
切分与向量化
TextSplitter 创建片段;Embeddings.embed_documents 编码文本;保存文本、metadata 与向量。
检索候选
查询进入 Retriever;向量存储实现完成相似度、阈值或 MMR 等搜索。
构建证据上下文
过滤权限、去重、可选 rerank,按上下文预算构造带来源 ID 的证据块。
生成与校验
模型根据证据答复;检查引用是否存在、是否支持结论,缺少证据时明确返回不足。
| 接口 | 输入 / 输出 | 关键机制 |
|---|---|---|
Document | 正文 + metadata。 | 来源不会自动随文本拼接而完整保留,应用要明确序列化方式。 |
RecursiveCharacterTextSplitter | 文本 → 片段。 | 依序尝试分隔符;长片段递归使用更细分隔符,短片段合并到预算内。 |
Embeddings | documents/query → 向量。 | 区分 embed_documents 与 embed_query;维度与归一化由具体模型决定。 |
VectorStore | 存储与搜索接口。 | 框架定义接口,具体索引算法和数据库能力由后端实现。 |
VectorStoreRetriever | query string → list[Document]。 | search_type 支持 similarity、similarity_score_threshold、mmr 等接口路径,后端是否实现仍要确认。 |
切分参数的实际含义
TextSplitter 的默认 length_function 是 len。因此默认 chunk_size 度量字符长度,不能当作模型 token 数。需要 tokenizer 度量时应显式采用对应切分/长度函数。
chunk_overlap 是片段重叠预算,不保证每个实际边界恰好重复固定数量字符。中文材料可以显式增加句号、逗号等分隔符;采购表格还需保护行列、页码和表头信息。
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 | 检索器包装为工具,模型决定是否检索和如何改写查询。 | 需要多轮探索;必须限制次数并检查最终证据。 |
| 显式工作流 | 应用固定路由与验收节点,局部允许模型决策。 | 审阅、合规、采购等需要强步骤约束的流程。 |
源码定位 Document · RecursiveCharacterTextSplitter._split_text · TextSplitter._merge_splits · Embeddings · BaseRetriever · VectorStoreRetriever
配置传播、追踪与质量评估
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
工程边界:预算、恢复与工具权限
| 问题 | 建议的责任归属 | 落地要点 |
|---|---|---|
| 任务预算 | 应用层 + 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、判断和执行记录作为结构化数据保存。规则判断应可回溯到证据;没有证据的结论不应靠自由文本掩盖。
源码定位 HumanInTheLoopMiddleware · ModelCallLimitMiddleware · ToolRuntime · RunnableRetry
从测试学习:契约与边界
阅读实现后,应回到 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.py | wrapper 注册顺序、内层重试、请求覆盖及状态更新组合。 |
| test_structured_output_retry_exceeds_max_retries | 结构化输出修正也必须有最大次数。 |
| test_response_format.py | Provider / Tool 策略、schema 与错误路径等结构化契约。 |
| test_return_direct_graph.py | return_direct 对构图与路由的影响。 |
| ChatModelUnitTests | 模型适配器的标准输入输出与能力接口。 |
# 在已固定版本的仓库中,先准备当前包环境
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 的交互与布局另行验收。
实践:从离线组合到真实 Agent
建议先验证数据与执行契约,再接入外部模型。下面两个示例分别用于理解 LCEL 的固定数据流与 Agent 的工具循环。
实验 A · 可在离线环境执行的 RAG 数据流
安装 langchain-core 后无需模型凭据。DemoRetriever 返回固定材料,FakeListChatModel 返回固定答案;这个实验验证组合接口与数据流,不验证向量检索效果或模型推理能力。
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 由环境配置,示例不会绑定随时间失效的固定型号。
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
迁移到 Java:保留语义,控制范围
以下是设计建议,用于自研 Java Agent Runtime。目标是迁移契约与执行语义;没有把 Python 类和完整 LangGraph 图引擎逐一照搬。
| LangChain / LangGraph 抽象 | 建议的 Java 抽象 | 必须保留的语义 |
|---|---|---|
| BaseChatModel / AIMessage | ModelClient、ModelResponse、StreamEvent。 | 供应商转换隔离;文本、工具调用、结束与错误有类型。 |
| Message 类型 | AgentMessage 与供应商 DTO 分离。 | 角色、消息 ID、tool_call ID、多模态及元数据明确。 |
| BaseTool / ToolNode | Tool + ToolRegistry + ToolExecutor。 | schema、输入校验、可信 context 注入、结构化结果。 |
| ModelRequest / wrappers | 不可变 ModelRequest + ModelInterceptor。 | 请求覆盖、顺序、短路与局部重试明确。 |
| AgentState / reducer | AgentState + 显式 StateUpdate。 | 增量消息与稳定 ID,不把状态与供应商消息格式混在一起。 |
| 条件边 | AgentLoop / NextAction。 | 模型决策、pending calls、工具结果与终止原因明确。 |
| Runtime.context | RunContext。 | 身份、tenant、deadline、取消令牌等由服务端提供。 |
| Checkpointer / Store | 后续独立接口。 | 先完成最小闭环,再扩展恢复和跨会话记忆。 |
// 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 和复杂工作流可作为独立阶段接入。
学习顺序与验收任务
先读契约,再运行离线组合
阅读 Runnable、Message、BaseChatModel、BaseTool;运行实验 A。验收:能解释 invoke/batch/stream 的差异及 ID 的作用。
沿一次工具调用读工厂
追踪 create_agent → model_node → _execute_model_sync → _build_commands → 条件边。验收:列出每一步的输入、输出和责任。
补齐失败和结束路径
阅读 invalid_tool_calls、return_direct 和 structured-output 测试。验收:为每个边界写出状态和路由预期。
理解扩展顺序与状态恢复
阅读 middleware、add_messages、checkpoint 和 streaming。验收:解释 wrapper 重试次数为何可能不同于模型节点次数。
完成可评估的业务闭环
接入实际检索与一个只读业务工具,再添加预算、权限、trace 和引用校验。验收:用固定样本解释成功率、证据支持率与成本。
建议的源码阅读路线
Runnable→RunnableSequence→RunnableParallel。AIMessage→ToolMessage→BaseChatModel.invoke。@tool→BaseTool._parse_input→ToolNode。create_agent→ 模型节点 →_build_commands→ 两个方向的条件边。AgentMiddleware→ wrapper 组合 → 结构化输出策略。add_messages→ retriever / splitter → 对应测试。
用解释能力检查学习结果
为什么绑定工具后,模型不能直接调用 Python 函数?
bind_tools 绑定的是名称、说明和参数 schema;模型返回 tool calls。客户端运行时通过 registry 匹配工具并执行,再把 ToolMessage 送入后续模型上下文。供应商内建工具属于另一种执行边界。
为什么不能把所有工具异常都自动返回给模型?
有的错误是可修复的参数问题,有的是内部程序错误,还有的是 interrupt 等控制流。框架的默认处理和应用策略必须区分这些情况;返回错误消息也应避免泄露内部细节。
为什么不能把 recursion_limit 当作模型调用次数?
图中还可能执行工具和 hook 节点;wrapper/SDK 又可能在一个模型阶段发起多个尝试。因此步数、模型阶段数和真实网络请求数需要分别统计。
为什么有 checkpoint 仍然可能重复业务动作?
持久化图状态和远端业务写入通常不在同一个原子事务里。崩溃或重试可能落在“业务已写入、状态未确认”的窗口,需要业务幂等和状态查询。
为什么 schema 正确仍然不代表审阅结果正确?
schema 校验检查类型与字段约束,无法证明 evidence 真实存在或足以支持 conclusion。引用完整性、规则判断和业务正确性需要独立验收。
什么时候应该选择显式工作流?
当步骤顺序、授权、人工批准、失败恢复和验收边界必须可控时,由应用明确安排图节点;模型决策仅放在需要灵活探索的局部阶段。
固定源码与参考资料
所有 LangChain 源码链接固定到 57236d55d9ffc8634bda987861e6fce460aff268。作为底层机制补充的 LangGraph 源码单独固定到 9a0394d88b2211f299dcd69df92db3480c69ee61;它是独立仓库快照,不宣称与本次主包 uv.lock 解析出的分发版本完全相同。
- Agent 工厂与路由
libs/langchain_v1/langchain/agents/factory.py - 中间件类型与请求契约
libs/langchain_v1/langchain/agents/middleware/types.py - 结构化输出策略
libs/langchain_v1/langchain/agents/structured_output.py - Runnable 与组合算子
libs/core/langchain_core/runnables/base.py - 运行配置
libs/core/langchain_core/runnables/config.py - ChatModel 核心执行
libs/core/langchain_core/language_models/chat_models.py - AI 消息与增量聚合
libs/core/langchain_core/messages/ai.py - 工具结果消息
libs/core/langchain_core/messages/tool.py - 工具输入与执行契约
libs/core/langchain_core/tools/base.py - 函数到工具的转换
libs/core/langchain_core/tools/convert.py - OpenAI 供应商适配
libs/partners/openai/langchain_openai/chat_models/base.py - Retriever 契约
libs/core/langchain_core/retrievers.py - VectorStore 与检索包装
libs/core/langchain_core/vectorstores/base.py - 递归切分器
libs/text-splitters/langchain_text_splitters/character.py - 工具执行错误转换
libs/langchain_v1/langchain/agents/middleware/tool_error.py - 模型阶段调用上限
libs/langchain_v1/langchain/agents/middleware/model_call_limit.py - 人工批准中间件
libs/langchain_v1/langchain/agents/middleware/human_in_the_loop.py - LangGraph:工具节点与 runtime
libs/prebuilt/langgraph/prebuilt/tool_node.py - LangGraph:消息 reducer
libs/langgraph/langgraph/graph/message.py
| 官方文档 | 补充内容 |
|---|---|
| LangChain Overview | 生态定位与标准模型接口。 |
| Event Streaming | v3 typed projections、模型消息与工具执行事件。 |
| Streaming | stream_mode 的执行进度、消息与自定义输出。 |
| Short-term Memory | checkpoint、thread 与会话状态使用方式。 |
| Middleware Overview | 生命周期扩展与包装模型/工具调用。 |