当 .pipe()不够用时:如何真正串联两条 LangChain 链?

53 阅读4分钟

在上一篇文章中,我们学会了用 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}`)
]);

现在,数据流动就完全受控了:

  1. 用户输入 { topic: "闭包" }
  2. explainChain 返回消息对象 → 我们取 .content
  3. 把纯文本传给 summaryChain → 它能正确识别 {explain}

问题解决了!

而且,你还可以在中间加入日志、错误处理、格式校验等逻辑,非常自由。


四、但等等——其实 .pipe() 也能做到!

看到这里,你可能会想:“原来 .pipe() 有局限,以后都用 RunnableSequence.from 吧。”

先别急!

其实,.pipe() 并没有“不能用”,只是我们少了一个关键环节转换节点

如果我们能在 explainChainsummaryChain 之间插入一个“提取 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 工作流的真正起点。