5. LCEL 链
Runnable 协议是 LangChain 的灵魂。所有东西(model、prompt、tool、parser)都实现 Runnable 接口:
interface Runnable {
invoke(input): Promise<output> // 接收输入,返回输出
stream(input): AsyncGenerator // 流式
batch(inputs): Promise<outputs> // 批量
}
因为大家都实现同样的接口,所以可以像管道一样拼起来:
const chain = prompt | model
// prompt 的输出 → model 的输入
pipe 这个词来自 Unix/Linux 的管道符 |。设想一条流水线:
原材料 → [工序A] → [工序B] → [工序C] → 成品
每个工序接收上一步的输出,作为自己的输入,处理完传给下一步。.pipe() 就是这么个东西:
A.pipe(B).pipe(C)
// 等价于:A 的输出 → 喂给 B → B 的输出 → 喂给 C
这让你能像搭积木一样构建复杂流程,而不用嵌套回调。
pipe 示例
import { ChatPromptTemplate } from '@langchain/core/prompts'
import { StringOutputParser } from '@langchain/core/output_parsers'
const prompt = ChatPromptTemplate.fromTemplate('用一句话解释 {topic}')
const chain = prompt.pipe(model).pipe(new StringOutputParser())
// 或用 | 管道符(需要 Runnable 类型支持,ts不支持这种写法):
// const chain = prompt | model | new StringOutputParser()
const result = await chain.invoke({ topic: 'Docker' })
// 直接是字符串(StringOutputParser 把 AIMessage.content 提取出来)
prompt.pipe(model).pipe(parser)在不 pipe 的情况下,等价于这样写:// 不用 pipe,手动一步步传 const step1 = await prompt.invoke({ topic: 'Docker' }) // step1 = [HumanMessage('...Docker...')] const step2 = await model.invoke(step1) // step2 = AIMessage { content: 'Docker 是...' } const step3 = await parser.invoke(step2) // step3 = 'Docker 是...'(纯字符串) console.log(step3)pipe 就是把上面这三步自动串起来,中间变量都不用写。所以:
// 这两段代码完全等价 const chain = prompt.pipe(model).pipe(parser) const result = await chain.invoke({ topic: 'Docker' }) // 等价于手动三步 const step1 = await prompt.invoke({ topic: 'Docker' }) const step2 = await model.invoke(step1) const step3 = await parser.invoke(step2)整个流程就是这样的:
chain.invoke({ topic: 'Docker' }) │ ├─ 1. 把 { topic: 'Docker' } 传给 prompt.invoke() │ → 返回 [HumanMessage('用一句话解释什么是 Docker?')] │ ├─ 2. 把上面这组消息传给 model.invoke() │ → 返回 AIMessage { content: 'Docker 是...' } │ └─ 3. 把这个 AIMessage 传给 parser.invoke() → 返回 'Docker 是...'(纯字符串)
注意:pipe 不是随便拼都行,他有个要求是:前一个的输出类型必须能喂给后一个。比如:
// ❌ 类型对不上
const badChain = model.pipe(prompt)
为什么错?
model的输出是AIMessageprompt的输入是{ topic: string }(对象)AIMessage不能当{topic}用 → 类型不匹配 → 运行时出错
正确的应该是这样:
// ✅ 类型能衔接
const goodChain = prompt.pipe(model)
// prompt 输出:消息 → model 输入:消息 ✅
和 ai SDK 对照
ai SDK 没有这种"链"的概念——它靠参数嵌套(比如 generateText({ model, messages, tools }))。LangChain 的 LCEL 是更通用的组合范式,学习成本高但灵活性强。
重点理解
- Runnable 协议 = LangChain 的统一接口,万物皆可 invoke/stream/batch。
- pipe /
|= 把多个 Runnable 拼成一个链。 - 后面的 RAG、Agent 本质上都是用 LCEL 拼出来的链或图。
完整示例:
import 'dotenv/config'
import { ChatDeepSeek } from '@langchain/deepseek'
import { ChatPromptTemplate } from '@langchain/core/prompts'
import { StringOutputParser } from '@langchain/core/output_parsers'
const model = new ChatDeepSeek({
model: 'deepseek-v4-flash',
apiKey: process.env.DEEPSEEK_API_KEY,
})
async function main() {
console.log('=== 1. 用 .pipe() 拼链 ===\n')
// prompt → model → parser 三段拼起来
// StringOutputParser 把 AIMessage.content 提取成纯字符串
const prompt = ChatPromptTemplate.fromTemplate('用一句话解释什么是 {topic}')
const chain = prompt.pipe(model).pipe(new StringOutputParser())
const result = await chain.invoke({ topic: 'Docker' })
console.log('类型:', typeof result) // string(不再是 AIMessage)
console.log('结果:', result)
console.log()
console.log('=== 2. 链可以复用 + 批量调用 ===\n')
// 链是 Runnable,支持 batch(并行调用多个输入)
const results = await chain.batch([
{ topic: 'Redis' },
{ topic: 'GraphQL' },
{ topic: 'WebSocket' },
])
results.forEach((r, i) => {
const topics = ['Redis', 'GraphQL', 'WebSocket']
console.log(`${topics[i]}: ${r}`)
})
console.log()
console.log('=== 3. 更复杂的链:带 system 设定 ===\n')
const chatPrompt = ChatPromptTemplate.fromMessages([
['system', '你是一个{role},回答要{style}'],
['human', '{question}'],
])
const smartChain = chatPrompt.pipe(model).pipe(new StringOutputParser())
const answer = await smartChain.invoke({
role: '技术作家',
style: '通俗易懂,用比喻',
question: '什么是消息队列?',
})
console.log(answer)
}
main().catch(err => {
console.error('运行失败:', err)
process.exit(1)
})
附1:OutputParser
前面的例子中,我们用到了 StringOutputParser,这是 LangChain 的 **Output Parser(输出解析器)**中的一个组件,其实还有很好几个这样的解析器,他们的作用都是是“接收模型输出,做某种转换”:
| Parser | 输入 | 输出 | 用途 |
|---|---|---|---|
StringOutputParser |
AIMessage | string | 提取纯文本 |
JsonOutputParser |
AIMessage (内容是 JSON 字符串) |
对象 | 解析 JSON |
CommaSeparatedListOutputParser |
AIMessage | string[] | 把逗号分隔的文本拆成数组 |
StructuredOutputParser |
AIMessage | 对象 | 按 schema 解析,老 API,现在不推荐 |
StringOutputParser 是其中最简单也是最常用的——只做一件事:AIMessage → string。
它们都在链里的位置:链的"出口",一条链通常长这样:
[prompt] → [model] → [parser]
入口 出口
- prompt 是入口:负责把你的输入(对象)变成模型能理解的消息
- model 是中间:负责生成回答
- parser 是出口:负责把模型的回答转成你想要的格式
所以 parser 永远在链的末尾,它决定你最终拿到什么类型:
- 不加 parser → 拿到 AIMessage
- 加 StringOutputParser → 拿到 string
- 加 JsonOutputParser → 拿到对象
和 .withStructuredOutput() 的区别
我们前面使用过 model.withStructuredOutput(bookListSchema),用来设定想要的返回数据格式。实际上他和我们前面说的 Output Parser 完全不是同一个东西。
-
OutputParser(输出解析器)——链尾的"转换器",是对返回的AIMessage 进行转换。
prompt → model → OutputParser ↑ 它在这里:链的最末端,一个独立的 Runnable 组件 -
withStructuredOutput ——model 的"特殊模式",他是model的一个方法,工作方式是:通过工具调用(tool_choice)强制模型直接输出符合 schema 的结构化数据,模型不是"先生成文本再被解析",而是直接生成结构化数据。
prompt → structuredModel(内部就用工具调用强制输出) ↑ 它改变了 model 本身的行为,不是链尾的独立组件
两者对比
| 维度 | OutputParser | withStructuredOutput |
|---|---|---|
| 是什么 | 链尾的独立 Runnable | model 的方法,返回新 model |
| 怎么工作 | 模型先自由生成,parser 再解析 | 强制模型用工具调用直接输出结构化 |
| 可靠性 | 低(模型可能生成不规范的 JSON) | 高(底层是工具调用,API 层面保证格式) |
| 在链里的位置 | 永远在最末端 | 替换掉 model 本身 |
| 在 pipe 里怎么用 | .pipe(parser) |
替换 model,.pipe(structuredModel) |
Q1: withStructuredOutput 在管道里怎么用?
直接替换掉 model 就行,不要再加 parser(加了反而会出错,因为 structuredModel 输出已经是对象,不是 AIMessage):
// ✅ 正确
const structuredModel = model.withStructuredOutput(schema)
const chain = prompt.pipe(structuredModel) // prompt 输出消息 → structuredModel 输出对象(直接)
const result = await chain.invoke({ ... }) // result 是 { name: '张三', age: 25 },直接是对象
// ❌ 错误:再接 StringOutputParser 会报错
const chain = prompt.pipe(structuredModel).pipe(new StringOutputParser())
// StringOutputParser 期望输入是 AIMessage,但 structuredModel 输出是对象,类型对不上
所以记住这条规则:withStructuredOutput 用在 pipe 里时,它就是链的终点,后面不能再接 parser。
附2:@langchain/community
LangChain 官方的"第三方集成大杂烩"包,把 LangChain 接各种外部服务(向量库、文档加载器、LLM、工具等)的代码做了统一的调用封装。
安装:
npm i @langchain/community --legacy-peer-deps
为什么必须加 --legacy-peer-deps?
community 声明了 115+ 个 peerDependencies(对等依赖)。其中 @browserbasehq/stagehand(一个浏览器自动化库)要求:dotenv@^16.4.5、zod@^3.23.8,其实这些版本已经很好了,如果你之前已有这几个包,就会报错装不上。
装了它到底装了多少东西?
常见误解:community 有 100+ peer,装它等于装了几十个包。
真相:实际只新增几个小包。
| 类型 | 数量 | npm 会装吗 |
|---|---|---|
dependencies(直接依赖) |
9 个 | ✅ 会装(@langchain/openai、langsmith、flat、uuid 等小包) |
peerDependencies(对等依赖) |
115 个 | ❌ 不会装(只是声明,npm 不主动安装) |
| 其中 optional peer | 几乎全部 | 缺失不报错 |
净增量:装 community 实际只增加约 5-7 个小包(几十 MB),那些"几百 MB"的传说,是把 peerDependencies 里 puppeteer/playwright/aws-sdk 这种不会被装的重型包算进去了的误解。
内部结构(22 个类别,249 个集成)
按数量排序,community 包含这些大类:
| 类别 | 数量 | 干什么的 | 独立包? |
|---|---|---|---|
| document_loaders | 44 | 文档加载器(PDF/Word/网页/Notion...) | ❌ 还在 community |
| vectorstores | 43 | 向量库(Chroma/Pinecone/PG/Faiss...) | ⚠️ 部分(Pinecone/MongoDB 已独立,Chroma 还在) |
| tools | 27 | 工具(搜索/计算/数据库查询...) | ❌ 大部分在 community |
| chat_models | 22 | 聊天模型(通义/Bedrock/TogetherAI...) | ⚠️ 主流已独立(@langchain/openai 等) |
| stores | 20 | 状态存储 | ❌ |
| llms | 19 | 传统 LLM | ❌ |
| embeddings | 18 | embedding 模型 | ❌ |
| retrievers | 12 | 检索器 | ❌ |
| memory | 5 | 对话记忆 | ❌ |
| agents | 5 | Agent 工具包(含 stagehand) | ❌ |
| 其它(caches/indexes/graphs/callbacks 等) | ~34 | 各种辅助 | ❌ |
community 是"适配器集合"
关键认知:community 提供"封装层",底层解析库要你自己装。
你的代码
│
▼
┌──────────────────────────────────────────────┐
│ @langchain/community(已装,不用再装) │
│ │
│ PDFLoader 类(封装代码) ← 这个有了 │
│ │ │
│ │ 调用 │
│ ▼ │
│ pdf-parse(底层库) ← 这个要自己装 │
└──────────────────────────────────────────────┘
- 第一层(封装代码):装 community 就有。提供 LangChain 统一接口(
load()返回Document[]等) - 第二层(底层解析库):用到才装。比如 PDF 解析靠
pdf-parse,浏览器控制靠puppeteer
为什么这么设计?
- 避免强制拉重型包(你只用 PDF 时,不该被迫装几百 MB 的 puppeteer)
- 版本解耦(
pdf-parsev1/v2 API 差很大,让你自己选) - 复用宿主已装的包(
chromadb你早装了,community 直接用)
类比:community 是"万能电源转接头",转接头买了就有,但每种插座(底层库)用到才装。
注意:目前这个包已经停止维护了。https://github.com/langchain-ai/langchainjs-community/issues/61,LangChain 团队正在把 community 拆成更细粒度的独立包(比如 @langchain/pinecone、@langchain/mongodb 等已经独立),很多集成(包括 Chroma、PDF 加载器)还没独立出来,只能继续用 community短期内 community 还能用(只是有警告),但长期会被逐步拆光。
已拆分的看一查看:https://docs.langchain.com/oss/javascript/integrations/providers/overview
💬 评论 0
还没有评论,快来抢沙发吧~