在上一篇文章中,我们学会了用 PromptTemplate.pipe(model) 构建一条简单的 AI 工作流。
现在,假设你已经成功创建了两条独立的链:
// 第一条链:详细解释前端概念
const explainChain = explainPrompt.pipe(model);
// 第二条链:把一段文本总结为三个要点
const summaryChain = summaryPrompt.pipe(model);
看起来一切顺利。
那么,如果你想把它们连起来——先解释,再总结——你会怎么做?
很自然地,你可能会写下这行代码:
const fullChain = explainChain.pipe(summaryChain);
毕竟,.pipe() 就是用来串联的,对吧?
但当你运行它,并传入 { topic: "闭包" } 时,却发现模型返回的内容完全不对——甚至可能报错。
这是为什么?
一、问题出在哪?让我们看看数据是怎么流动的
.pipe() 的工作方式很简单:把前一个链的完整输出,直接作为后一个链的输入。
所以当 explainChain.invoke({ topic: "闭包" }) 执行后,它返回的是一个 AI 消息对象,比如:
{
content: "闭包是指函数能够访问其词法作用域中的变量……",
role: "assistant"
}
而 summaryChain 的提示模板长这样:
const summaryPrompt = PromptTemplate.fromTemplate(`
请将以下内容总结为3个核心要点:
{explain}
`);
它期望的输入是一个包含 explain 字段的对象,比如:
{ explain: "闭包是指……" }
但 .pipe() 却把整个消息对象 { content: "...", role: "..." } 直接塞给了 summaryChain!
结果就是:
模板中的 {explain} 找不到值,变成 undefined,模型看到的 prompt 是:
请将以下内容总结为3个核心要点:
undefined
难怪回答乱七八糟!
那怎么办呢?
二、我们需要的不是“原样传递”,而是“提取 + 转换”
真正的需求是:
把
explainChain返回的content字段提取出来,包装成{ explain: "..." },再传给summaryChain
但 .pipe() 本身不支持中间处理——它只是机械地传递数据。
那如何去实现这个“提取”动作呢?
这时候,LangChain 提供了一个更灵活的工具:RunnableSequence.from。
三、用 RunnableSequence.from 精准控制每一步
RunnableSequence.from 允许你显式定义流程中的每一个步骤,每个步骤都可以是一个函数。
我们可以这样写:
const fullChain = RunnableSequence.from([
// 第一步:调用 explainChain,获取解释文本
(input) => explainChain.invoke({ topic: input.topic }).then(res => res.content),
// 第二步:把文本包装成 summaryChain 需要的格式
(explanation) => summaryChain.invoke({ explain: explanation }.then(res=>`知识点:${explaination} 总结:${res.text}`)
]);
现在,数据流动就完全受控了:
- 用户输入
{ topic: "闭包" } explainChain返回消息对象 → 我们取.content- 把纯文本传给
summaryChain→ 它能正确识别{explain}
问题解决了!
而且,你还可以在中间加入日志、错误处理、格式校验等逻辑,非常自由。
四、但等等——其实 .pipe() 也能做到!
看到这里,你可能会想:“原来 .pipe() 有局限,以后都用 RunnableSequence.from 吧。”
先别急!
其实,.pipe() 并没有“不能用”,只是我们少了一个关键环节:转换节点。
如果我们能在 explainChain 和 summaryChain 之间插入一个“提取 content”的小函数,.pipe() 就能正常工作。
而 LangChain 正好提供了这样的能力——任何函数都可以被 .pipe() 连接,只要它返回一个值或 Promise。
于是,我们可以这样写:
const extractContent = (message) => message.content;
const fullChain = explainChain
.pipe(extractContent)
.pipe((text) => ({ explain: text }))
.pipe(summaryChain);
或者更简洁地合并:
const fullChain = explainChain
.pipe((msg) => summaryChain.invoke({ explain: msg.content }));
看!我们依然用 .pipe() 完成了整个流程,只是中间加了一个“适配器”。
五、所以,到底该用哪个?
现在真相大白了:
.pipe()本身没问题,但它要求前后链的输入输出结构匹配- 如果不匹配,你就需要手动插入转换逻辑
RunnableSequence.from则是把这种“转换”显式化、集中化,更适合复杂流程
那该如何选择?
- 如果只是简单串联,且数据格式天然对齐 → 用
.pipe() - 如果需要提取字段、重组结构、加日志或条件分支 → 用
RunnableSequence.from - 或者,你也可以用
.pipe()+ 中间函数,保持链式风格
本质上,它们是同一能力的两种表达方式。
六、小结:工作流的核心是“数据适配”
这次的问题告诉我们一个关键原则:
在 LangChain 中,连接链的关键不是“能不能串”,而是“数据是否对得上”。
就像水管对接,如果口径不一样,你就需要一个“转接头”。
这个“转接头”,可以是一个函数,可以是一个 RunnableMap,也可以是 RunnableSequence 中的一环。
而理解这一点,才是构建可靠 AI 工作流的真正起点。