LangChain 1.0 终于出了。官方的定位很明确:
"LangChain v1 is a focused, production-ready foundation for building agents."
这次升级不是简单的 API 换个名字,而是把过去一整年踩坑总结的经验,重新压缩成一套以 agent 为中心的核心能力:
- ✅
createAgent:统一的 Agent 构建入口 - ✅ Middleware:横切能力(日志、HITL、摘要、安全...)的标准化
- ✅ Structured Output + Standard Content:结构化输出 & 多模态内容统一抽象
- ✅ 精简包结构:
langchain专注 agent,历史包迁到@langchain/classic(LangChain 文档)
这篇文章会完全基于 LangChain 官方文档,按照 JS v1 migration guide 的结构,把每一条变化讲清楚,再加上一些自己的理解,帮你从 0.0.x 的世界平稳过渡到 1.0。
一、核心变化概览:从表格看 v1 心智模型
JS 版 v1 migration guide 一上来就给了一个总表,基本涵盖了所有关键改动:(LangChain 文档)
| 主题 | v1 里"变了什么" |
|---|---|
| Import path | 预构建 agent 从 @langchain/langgraph/prebuilts 移到 langchain |
| Prompts | prompt 改名 systemPrompt;动态 prompt 用 middleware 实现 |
| Pre/Post-model hook | 统一变成 middleware 的 beforeModel / afterModel |
| Custom state | 用 middleware 的 stateSchema + Zod 定义 |
| Model | 动态模型选择挪到 wrapModelCall;不再推荐"预先 bind 工具的 model" |
| Tools | tools 接收 tool()、Tool 实例和 provider 内建工具;错误交给 wrapToolCall |
| Structured output | 内联到主循环,新增 toolStrategy / providerStrategy |
| Streaming | 流式事件中的节点名从 "agent" 改为 "model" |
| Runtime context | 配置改用 context 字段,不再鼓励 config.configurable |
| Standard content | 新增 message.contentBlocks 标准内容块 |
| Namespace | langchain 精简为 agent 核心能力,其余迁到 @langchain/classic |
| Breaking changes | Node 18 下线、新打包输出、旧 API 移除等 |
下面我按这些主题,一块块展开。
二、createAgent:统一的 Agent 入口 + Middleware 基础设施
2.1 从 createReactAgent 到 createAgent
旧世界里,你如果用的是 LangGraph 的预构建 ReAct agent,一般是这样引:(LangChain 文档)
typescript
// v0
import { createReactAgent } from "@langchain/langgraph/prebuilts";
在 v1 里,它变成了 LangChain 的一等公民:
typescript
// v1
import { createAgent } from "langchain";
const agent = createAgent({
model: "claude-sonnet-4-5-20250929",
tools: [getWeather],
systemPrompt: "You are a helpful assistant.",
});
我的理解:
- 以前"agent"是 LangGraph 的预构建,现在官方把这个预构建接回 LangChain,使得:
- LangChain = 高层 API(createAgent)
- LangGraph = 低层 graph runtime
- 你不再需要先学 LangGraph 才能用一个像样的 agent,但仍然可以随时"下潜"到 LangGraph 做高级控制。
2.2 Prompt:prompt → systemPrompt + 动态 prompt 中间件
文档里把 prompt 的变化拆成三块:静态重命名、SystemMessage、动态 prompt。(LangChain 文档)
2.2.1 静态 system prompt 重命名
- 旧:
prompt - 新:
systemPrompt
typescript
const agent = createAgent({
model,
tools,
systemPrompt: "You are a helpful assistant.",
});
为什么要改名?
prompt这个词太宽泛,谁都可以叫 prompt;systemPrompt明确告诉你:这只是系统级前置指令,用户 history / 工具反馈等都是别的层处理。
2.2.2 SystemMessage 行为简化
文档提到:如果你在 system prompt 中仍然用 SystemMessage,v1 会直接取其中的 string 内容 ,不再绕一圈。(LangChain 文档)
这其实是在消除"一堆 message 对象嵌套在一起"的复杂度,把 system 指令都归一成纯文本,便于统一注入到模型的 system 部分。
2.2.3 动态 prompt:交给 middleware 处理
动态 prompt(根据上下文、用户角色、状态动态改 system prompt)被认定为"核心 context engineering 模式",官方给了专门的 middleware:dynamicSystemPromptMiddleware (LangChain 文档)
示意代码(截取一下重点):
typescript
const contextSchema = z.object({
userRole: z.enum(["expert", "beginner"]).default("beginner"),
});
const userRolePrompt = dynamicSystemPromptMiddleware(
(_state, runtime) => {
const userRole = runtime.context.userRole;
const base = "You are a helpful assistant.";
if (userRole === "expert") return `${base} Provide detailed technical responses.`;
if (userRole === "beginner") return `${base} Explain concepts simply and avoid jargon.`;
return base;
}
);
const agent = createAgent({
model,
tools,
middleware: [userRolePrompt],
contextSchema,
});
我的理解:
- 动态 prompt 本质上是一个典型的 cross-cutting concern(横切关注点):它会影响所有 model 调用;
- v1 把它收敛到 middleware,而不是随便塞在 agent 逻辑里;
- 和你之前写的「按用户角色切换系统提示」需求,完美对上号。
2.3 Pre-model / Post-model hook → beforeModel / afterModel middleware
以前的"前/后 hook"现在都统一到 middleware 的两个钩子里:(LangChain 文档)
| 旧名 | 新名 | 位置 |
|---|---|---|
| pre-model hook | beforeModel |
middleware |
| post-model hook | afterModel |
middleware |
2.3.1 beforeModel:模型调用前做的事
典型用途(文档列了三个):(LangChain 文档)
- 摘要对话历史
- 修剪消息(trim)
- 输入安全(比如 PII 脱敏)
官方内置了一个 summarizationMiddleware,用法你已经看过:
typescript
const agent = createAgent({
model: "claude-sonnet-4-5-20250929",
tools,
middleware: [
summarizationMiddleware({
model: "claude-sonnet-4-5-20250929",
trigger: { tokens: 1000 },
}),
],
});
我的理解:
- 以前你可能会在每次调用前手动
slice messages或自己维护 session summary; - 现在这些都变成"插一个 middleware 就行"的模式;
- 逻辑上属于"请求前过滤 / 缩减 / 丰富"的一类,非常适合做 cross-cutting。
2.3.2 afterModel:模型返回后做的事
典型用途:(LangChain 文档)
- Human-in-the-loop(人审)
- 输出安全(敏感内容过滤)
内置例子是 humanInTheLoopMiddleware,你之前已经研究过:
typescript
const agent = createAgent({
model: "claude-sonnet-4-5-20250929",
tools: [readEmail, sendEmail],
middleware: [
humanInTheLoopMiddleware({
interruptOn: {
sendEmail: { allowedDecisions: ["approve", "edit", "reject"] },
},
}),
],
});
我的理解:
- 所有"模型说完话之后还要人兜底"的逻辑(HITL、guardrail、合规审计),都归到
afterModel; - 在流式/长流程 agent 中,这样的 hook 可以叠加多个,把风险逻辑模块化。
2.4 Custom state:stateSchema + middleware,别再塞到 createAgent
文档写得很清楚:
"Custom state is now defined in middleware using the
stateSchemaproperty. Use Zod to declare additional state fields that are carried through the agent run." (LangChain 文档)
示例(节选):
typescript
const UserState = z.object({
userName: z.string(),
});
const userState = createMiddleware({
name: "UserState",
stateSchema: UserState,
beforeModel: (state) => {
const name = state.userName;
// ...根据 name 动态改 prompt 或做别的事
},
});
我的理解:
- v0 时,custom state 一般是在
createAgent上用stateSchema一次性声明; - v1 更推荐:谁需要 state,就在哪个 middleware 上声明 stateSchema;
- 好处:
- 状态和功能天然绑定(一个中间件就认领自己那块状态);
- 重用中间件时,不会往全局 state 里乱加字段;
- TypeScript / Zod 类型也更干净。
Python 那边也同步要求:自定义 state 必须是 TypedDict,而不再接受 Pydantic / dataclass,理由也是为了简单一致。
2.5 模型相关:动态选型 & 不再支持"预先绑定工具的 Model"
2.5.1 动态模型选择:用 wrapModelCall
旧时代,你可能在 model 参数里传一个函数,根据输入动态选择模型。v1 把这个逻辑统一搬进 middleware:(LangChain 文档)
typescript
const dynamicModel = createMiddleware({
name: "DynamicModel",
wrapModelCall: (request, handler) => {
const count = request.state.messages.length;
const model = count > 10 ? "openai:gpt-5" : "openai:gpt-5-nano";
return handler({ ...request, model });
},
});
我的理解:
- "按对话长度 / 用户付费等级 / 请求类型,动态切模型"是典型 cross-cutting concern;
- 统一放到
wrapModelCall,你就可以:- 在同一个 agent 上轻松开关不同"模型策略";
- 做 AB test / 灰度发布时只换一个 middleware。
2.5.2 不要再传"已经 bind 工具的 model"
文档原话:(LangChain 文档)
为了更好支持 structured output,
createAgent应该接收"纯 model + 单独的 tools 列表",不要再用事先.bindTools(...)的 model。
旧写法:
typescript
// ❌ v1 不再支持
const modelWithTools = new ChatOpenAI({ model: "gpt-4o-mini" }).bindTools([someTool]);
const agent = createAgent({ model: modelWithTools, tools: [] });
新写法:
typescript
// ✅ 推荐
const agent = createAgent({ model: "gpt-4o-mini", tools: [someTool] });
我的理解:
- 结构化输出(尤其是用 ToolStrategy 时)的内部实现,需要框架统一管理"这次 LLM 调用到底有哪些 tools";
- 如果你传进来的是"外面已经 bind 过 tools 的 model",LangChain 很难在内部安全地追加/调整工具列表;
- 所以强制你:model 只负责"哪一家的哪个版本",tools 列表交给 createAgent。
2.6 Tools:三种来源 + 错误处理统一用 wrapToolCall
2.6.1 tools 支持的三种形态
v1 中,createAgent 的 tools 参数可以接:(LangChain 文档)
- 用
tool()包出来的函数(最推荐) - LangChain Tool 实例
- Provider 内建工具的"配置对象"
这让你可以写出很统一的代码:
typescript
const search = tool(
({ query }) => `Results for: ${query}`,
{
name: "search",
description: "Search the web",
schema: z.object({ query: z.string() }),
}
);
const agent = createAgent({
model: "gpt-4o",
tools: [search, dbTool, openAIFileSearchTool],
});
我的理解:
- 不管工具是你自己写的、从 @langchain/community 拿的、还是 provider 内建的,现在都能放进同一个
tools数组里统一调度; - 这也是为后面
llmToolSelectorMiddleware等中间件做铺垫。
2.6.2 工具错误处理:wrapToolCall 中间件
文档里专门有一小节:Handling tool errors ,推荐用 wrapToolCall 中间件来统一兜错误:(LangChain 文档)
官方示例思路是:
typescript
const handleToolErrors = createMiddleware({
name: "HandleToolErrors",
wrapToolCall: async (request, handler) => {
try {
return await handler(request);
} catch (error) {
return new ToolMessage({
content: `Tool error: Please check your input and try again. (${error})`,
tool_call_id: request.toolCall.id!,
});
}
},
});
我的理解:
- 不用在每个 tool 里都写 try/catch,然后返回一堆"错误字符串";
- 工具真报错时,转成一条
ToolMessage回给模型,让模型自己决定:- 重试?
- 换工具?
- 直接向用户道歉?
- 工具错误处理也被归类为 cross-cutting concern,交给 middleware 这一层统一管理。
2.7 Runtime context:告别 config.configurable
v1 引入了 context 配置:(LangChain 文档)
typescript
const agent = createAgent({
model: "gpt-4o",
tools,
contextSchema: z.object({ userId: z.string(), sessionId: z.string() }),
});
const result = await agent.invoke(
{ messages: [new HumanMessage("Hello")] },
{ context: { userId: "123", sessionId: "abc" } },
);
而以前你可能是这样:
typescript
agent.invoke(input, { configurable: { userId: "123" } });
我的理解:
config.configurable这个名字又长又模糊;context则一眼能看出:这是"这次执行的上下文配置",只读、静态;- 在 middleware 的
runtime.context里,你也能统一访问这些字段(比如 userRole、tenantId、业务开关等)。
2.8 Streaming node name:"agent" → "model"
这个细节很小,却很容易踩坑:
"When streaming events from agents, the node name was changed from
"agent"to"model"." (LangChain 文档)
如果你有在前端 / 后端按 node 名过滤流式事件,记得从:
typescript
if (event.name === "agent") { ... }
改成:
typescript
if (event.name === "model") { ... }
原因也很直白:这个 node 实际做的,就是"调用模型",叫 "agent" 容易和整个系统混淆。
三、Structured Output:主循环内联 + ToolStrategy / ProviderStrategy
结构化输出这块,文档重点讲了三件事:(LangChain 文档)
- Node 变化:不再单独一个 node,多余的一次 LLM 调用被干掉
- 策略:
toolStrategy&providerStrategy - Prompted output 移除:禁止"单纯用 prompt 硬控格式"
3.1 Node 层面的变化:不再多调一次 LLM
原文:
"Structured output used to be generated in a separate node... Structured output is generated in the main loop (no extra LLM call), reducing cost and latency." (LangChain 文档)
以前:
- agent 主循环跑完,再搞一个"结构化输出 node";
- 等于:多调一次模型,把自然语言再转成结构化 JSON。
现在:
- 结构化输出直接在 agent 的主循环内部完成;
- 因此:
- 延迟降低;
- 费用降低;
- 代码逻辑也简单了(不再有"最后多一个 node"的心智负担)。
3.2 两种策略:ToolStrategy vs ProviderStrategy
文档里明确区分了两种 structured output 策略:(LangChain 文档)
toolStrategy(schema)
→ 用"人工工具调用"来让模型按 schema 输出(即我们之前聊的"伪造一个结构化输出工具")providerStrategy(schema)
→ 用模型提供商原生的 JSON Schema / 结构化输出能力(比如 OpenAI 的.response_format)
示例(toolStrategy):
typescript
import { createAgent, toolStrategy } from "langchain";
import * as z from "zod";
const OutputSchema = z.object({
summary: z.string(),
sentiment: z.string(),
});
const agent = createAgent({
model: "gpt-4o-mini",
tools,
responseFormat: toolStrategy(OutputSchema),
});
更简单的情况:你也可以直接传 Zod schema,LangChain 会根据当前模型是否支持原生结构化输出自动选择策略:
typescript
const agent = createAgent({
model: "gpt-4o-mini",
tools,
responseFormat: weatherSchema, // 自动选策略
});
我的理解:
toolStrategy更通用,只要模型支持 tool calling 就能用;providerStrategy更严格,依赖具体 provider 的 JSON-schema 能力;- v1 把这俩抽象成"策略对象",而不是让你手写"怪异的 responseFormat 字符串"。
3.3 Prompted output 被移除:不再鼓励"靠 prompt 硬控 JSON"
文档开门见山:
"Prompted output via custom instructions in
responseFormatis removed in favor of the above strategies." (LangChain 文档)
之前:
- 有人会在
responseFormat里塞一段说明:"请按以下 JSON 返回......",本质上只是多了点 prompt; - 不稳定、难校验------一旦模型输出带多余文本 / markdown,就解析炸。
现在:
- 要结构化输出,就明确选策略 + schema;
- 返回结果统一出现在 agent state 的
structuredResponse里,并且经过 Zod / TypedDict 校验。
四、Standard Content Blocks:跨厂商统一的消息内容模型
这是 v1 另一个非常重要但很容易忽视的改动:标准内容块(standard content blocks)。
4.1 问题:不同 provider 的消息格式结构各不相同
现代 LLM 输出已经不是单纯一串文本,而是:
- reasoning / thinking 块
- citations(来源引用)
- 多模态(image/audio/video)
- 内置工具调用结果(代码执行、搜索等)
每家 provider 的结构都不一样,比如:
- Anthropic 的 content block 有
thinkingtype; - OpenAI 自己有一套 annotations 格式;
- Amazon Nova 也有自定义的 content block 结构。
如果你直接用 message.content,就会变成各种 if(provider === "anthropic") 的地狱。
4.2 v1:message.contentBlocks 标准化视图
JS 迁移文档里写得很清楚:(LangChain 文档)
- 新增
contentBlocks属性:provider-agnostic 标准内容块- 新增
ContentBlock.*TypeScript 类型- 可以通过环境变量 / 配置把 standard block 序列化回
content
使用方式:
typescript
const model = await initChatModel("gpt-5");
const response = await model.invoke("Explain AI");
for (const block of response.contentBlocks) {
if (block.type === "reasoning") {
console.log(block.reasoning);
} else if (block.type === "text") {
console.log(block.text);
}
}
构造多模态输入:
typescript
const message = new HumanMessage({
contentBlocks: [
{ type: "text", text: "Describe this image." },
{ type: "image", url: "https://example.com/image.jpg" },
],
});
const res = await model.invoke([message]);
我的理解:
contentBlocks就是"跨 provider 的统一 AST";- 如果你要做:
- 展示 reasoning trace;
- 统一 UI 上的图文混排;
- 把 OpenAI / Anthropic / Nova 的输出全部喂给前端;
- 那就应该用
contentBlocks,而不是继续手搓解析各家的原始content结构。
4.3 content 还在吗?如何序列化 standard blocks?
文档强调:
"The existing
message.contentfield remains unchanged for strings or provider-native structures." (LangChain 文档)
也就是说:
- 老代码里直接
msg.content拿字符串的逻辑不会被破坏; - 新的
contentBlocks是额外的一层视图,需要时再用。
如果你想把 standard block 序列化回 content(比如要通过网络传给前端,只想看一个字段),可以设置:(LangChain 文档)
bash
export LC_OUTPUT_VERSION=v1
# 或在 JS 初始化模型时 outputVersion: "v1"
五、包结构精简:langchain & @langchain/classic
5.1 langchain:只做 agent 核心
v1 概览文档和 migration guide 一致强调:
"The
langchainpackage namespace is streamlined to focus on agent building blocks."
目前 JS 版 langchain 暴露的内容主要包括:
- Agents:
createAgent,AgentState - Messages:消息类型、
trimMessages、content blocks(re-export 自@langchain/core) - Tools:
tool+ 部分工具基类 - Models:
initChatModel,BaseChatModel
我的理解:
langchain现在更像一个"agents SDK",聚焦在:- 接模型
- 接工具
- 用 middleware 拼装逻辑
- 其他传统功能(chains、retrievers、indexing...)都挪到 classic 包。
5.2 @langchain/classic:老世界的"博物馆"
文档说明:
如果你还在用:
- 传统 chains
- indexing API
- 原来从
@langchain/communityre-export 的东西现在请安装
@langchain/classic,从那里 import。
迁移方式大致是:
typescript
// v0
import { stuffDocumentsChain } from "langchain/chains";
// v1
import { stuffDocumentsChain } from "@langchain/classic/chains";
我的理解:
@langchain/classic有点像"LTS 老版本":- 功能保留;
- 发布节奏相对稳定;
- 但不再是"未来重点投资源的地方",新特性基本都围绕 agent / middleware / content blocks 展开。
- 对你自己项目来说:
- 新东西尽量用
langchain+@langchain/core; - 老代码可以渐进式迁移,一时半会迁不动就挂在
@langchain/classic上继续用。
- 新东西尽量用
5.3 删除的废弃 API
migration guide 还给了一大串已经被删掉的 API 列表,比如:
- Core:
TraceGroup→ 用 LangSmith tracingRemoteRunnable→ 直接砍掉
- Runnables:
Runnable.bind→ 改用.bindTools()等专门方法Runnable.map→ 用.batch()
- Chat models / LLM:
predict,predictMessages,call等 → 全部用.invoke()统一起来
- Retrievers:
getRelevantDocuments→.invoke()
我的理解:
.invoke()成为统一调用入口,是 LangChain 这两年一直在做的"API 收敛";- 如果你现在还在到处
predict、call、getRelevantDocuments,v1 是非常好的时机统一清理。
六、平台 & 构建层:Node 20、Bundler 输出、新的 release 政策
6.1 Node 18 下线,Node 20+ 才能用
JS 侧 changelog 和 migration guide 都写了:
所有 LangChain 包现在都要求 Node.js 20 或更高版本。Node 18 已在 2025 年 3 月 EOL。
这意味着:
- 在你的项目里,如果还锁在 Node 18,升级 LangChain 1.0 会直接失败;
- 建议顺手也把其它依赖检查一下(比如 Vite、Next.js 等),一并升级到 Node 20 系列。
6.2 新的构建输出:从 tsc 散装文件到 bundler
原文:
"Builds for all langchain packages now use a bundler based approach instead of using raw typescript outputs. If you were importing files from the
dist/directory... you will need to update your imports to use the new module system."
简单翻译一下:
- 以前:
src→ tsc →dist,目录结构几乎一样,你可以(虽然不推荐):
typescript
import { X } from "langchain/dist/whatever/internal.js";
- 现在:
- 用 bundler(tsup / rollup 那一流)根据少数入口打包;
dist/不再对应源码结构,很多内部文件路径直接消失;- 你必须只通过官方导出的模块入口来用包。
我的理解:
- 这是在从工程上彻底扼杀"深度 import 内部文件"的行为(不然一升级就爆);
- 同时也带来了更好的 tree-shaking 和多环境兼容,对前端 bundle 体积是利好。
七、如何实际迁移自己的项目?
最后给一个实战向的 checklist,方便你把这篇博客当"迁移 todo list"。
- 升级依赖 & Node 版本
- Node 升到 20+;
langchain&@langchain/core升到最新 v1;(LangChain 文档)
- **把
createReactAgent换成 **createAgent- import 路径改到
"langchain"; - 把原来传给
model的动态函数、pre/post hook,迁到 middleware 上; - prompt 改为
systemPrompt,动态 prompt 用dynamicSystemPromptMiddleware。
- import 路径改到
- 工具相关
- 不再使用
model.bindTools()传进 agent; - 把所有工具放进
tools: [...]; - 如果有统一的错误兜底逻辑,用一个
wrapToolCall中间件代替散装 try/catch。
- 不再使用
- 结构化输出
- 如果以前用的是 "prompt 里自己写 JSON 规范 + 手动 parse",现在改:
responseFormat: zodSchema或responseFormat: toolStrategy(schema)
- 从返回的
structuredResponse里取结果。
- 如果以前用的是 "prompt 里自己写 JSON 规范 + 手动 parse",现在改:
- 消息 & 多模态
- 如果开始接入 reasoning、多模态、内置工具结果:统一用
contentBlocks; - 给前端传数据时,如果只想看到一个字段,可以通过
LC_OUTPUT_VERSION=v1把 block 序列化进content。
- 如果开始接入 reasoning、多模态、内置工具结果:统一用
- 包结构 & 旧功能
- 任何
langchain/chains之类的 import,都迁到@langchain/classic; - 把所有
.predict/.call/.getRelevantDocuments改成.invoke()。
- 任何
- Streaming / 监控
- 如果在用
streamEvents()或类似接口,记得把 node 名从"agent"改成"model"; - tracing 相关 API 改用 LangSmith。
- 如果在用
总结:LangChain 1.0 是一次「收敛」而不是「推翻重来」
从官方的 release 文档来看,v1 的主线非常清晰:
- Agent 是一等公民:
createAgent+ LangGraph runtime - Middleware 成为承载 cross-cutting concern 的标准机制
- Structured output 和 Standard content 负责把"模型输出 → 应用可用数据"的链路拉直
- 包结构和构建方式收敛到一个更可维护、更稳定的形态
如果你这段时间一直在围绕「Agent + 工具 + 上下文 + 人审 + 安全」做事情,那 v1 基本就是把你常写的那堆"胶水逻辑",正式升级为框架的 first-class feature。