LangChain Chain 与 Runnable:把大模型调用串成流水线

模型会调了,提示词模板也会写了,但真正的魔法在于——把一堆组件串起来,让数据像流水线一样自动流转。这就是 LangChain 最核心的概念:Chain(链)和 Runnable(可运行接口)。本文带你从"一个组件一个组件手动调"进化到"一条管道全自动处理",感受 LCEL 管道符的真正威力。


一、Chain 是什么?为什么需要它?

1.1 先看没有 Chain 的写法

假设你要做一个"翻译 + 摘要"的功能——先把中文翻译成英文,再对英文做摘要。没有 Chain 的话,你得这么写:

from langchain.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4o-mini")

# 第一步:翻译
translate_prompt = ChatPromptTemplate.from_template(
    "将以下中文翻译成英文:{text}"
)
translate_messages = translate_prompt.format_messages(text="LangChain 是一个强大的大模型应用开发框架")
translate_result = llm.invoke(translate_messages)

# 第二步:摘要(需要手动拿上一步的输出当输入)
summary_prompt = ChatPromptTemplate.from_template(
    "用一句话概括以下英文内容:{english_text}"
)
summary_messages = summary_prompt.format_messages(english_text=translate_result.content)
summary_result = llm.invoke(summary_messages)

print(summary_result.content)

每一步的输出都要手动取出来,再塞进下一步的输入——两步就这样,十步呢? 代码会变成一堆中间变量,像意大利面条一样缠在一起。

1.2 有了 Chain 之后

同样的逻辑,用 LCEL 管道符 | 串起来:

from langchain.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain.schema.output_parser import StrOutputParser

llm = ChatOpenAI(model="gpt-4o-mini")

# 定义两条独立的链
translate_chain = (
    ChatPromptTemplate.from_template("将以下中文翻译成英文:{text}")
    | llm
    | StrOutputParser()
)

summary_chain = (
    ChatPromptTemplate.from_template("用一句话概括以下英文内容:{english_text}")
    | llm
    | StrOutputParser()
)

# 调用:数据自动流转
english = translate_chain.invoke({"text": "LangChain 是一个强大的大模型应用开发框架"})
summary = summary_chain.invoke({"english_text": english})

print(f"翻译:{english}")
print(f"摘要:{summary}")

Chain 的本质:把多个组件串成一条流水线,每个组件的输出自动变成下一个组件的输入。 你不用再手动管数据传递,只管"把什么拼到什么后面"。


二、管道符 | 的魔法:LCEL 深入

LCEL(LangChain Expression Language)的核心就是管道符 |。它的设计灵感来自 Unix 管道——cat file | grep key | sort,数据从左流到右。

2.1 管道符的底层:Runnable 接口

所有能用 | 拼接的组件,都实现了同一个接口:Runnable。这个接口定义了统一的调用方法:

方法行为适用场景
invoke(input)同步调用,一次性返回普通脚本、调试
ainvoke(input)异步调用FastAPI、异步 Web 框架
stream(input)同步流式输出实时展示生成内容
astream(input)异步流式输出异步 + 流式
batch(inputs)批量处理多条数据批量翻译、批量分析

2.2 一条链,四种调用方式

from langchain.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain.schema.output_parser import StrOutputParser

prompt = ChatPromptTemplate.from_template("给一个关于{topic}的冷笑话")
llm = ChatOpenAI(model="gpt-4o-mini")
chain = prompt | llm | StrOutputParser()

# 方式一:invoke(同步)
result = chain.invoke({"topic": "程序员"})

# 方式二:stream(流式,一个字一个字往外蹦)
for chunk in chain.stream({"topic": "程序员"}):
    print(chunk, end="", flush=True)

# 方式三:batch(批量处理)
results = chain.batch([
    {"topic": "程序员"},
    {"topic": "产品经理"},
    {"topic": "设计师"},
])

# 方式四:ainvoke(异步)
import asyncio
async def main():
    result = await chain.ainvoke({"topic": "程序员"})
    print(result)
asyncio.run(main())

一条链写好,四种调用方式全都有。 这就是 Runnable 接口的威力——你不需要为每种调用方式写不同的代码,接口层帮你全搞定了。


三、输出解析器:让模型输出"结构化"

默认情况下,模型返回的是纯文本。但很多时候你需要的是结构化的数据——JSON、列表、字典。输出解析器就是干这个的。

3.1 StrOutputParser:最简单的解析器

把模型输出的消息对象转换成人话字符串:

from langchain.schema.output_parser import StrOutputParser

chain = prompt | llm | StrOutputParser()

# 没有 StrOutputParser → 返回 AIMessage 对象
# 有 StrOutputParser → 返回纯字符串
result = chain.invoke({"topic": "Python"})
print(type(result))  # <class 'str'>

3.2 JsonOutputParser:让模型输出 JSON

from langchain.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain.schema.output_parser import JsonOutputParser
from pydantic import BaseModel, Field

# 定义期望的数据结构
class Movie(BaseModel):
    title: str = Field(description="电影名称")
    director: str = Field(description="导演")
    year: int = Field(description="上映年份")
    rating: float = Field(description="豆瓣评分,1-10分")

# 创建解析器
parser = JsonOutputParser(pydantic_object=Movie)

# 模板中注入格式说明
prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个电影数据库。请按以下JSON格式输出:\n{format_instructions}"),
    ("human", "介绍一下电影《肖申克的救赎》"),
])

# 组装链
chain = prompt | llm | parser

result = chain.invoke({
    "format_instructions": parser.get_format_instructions(),
})

print(result)
# {'title': '肖申克的救赎', 'director': '弗兰克·德拉邦特', 'year': 1994, 'rating': 9.7}

3.3 常用解析器一览

解析器输入输出典型场景
StrOutputParserAIMessagestr日常对话、文本生成
JsonOutputParserAIMessagedict结构化数据提取
CommaSeparatedListOutputParserAIMessagelist提取关键词列表
DatetimeOutputParserAIMessagedatetime提取日期时间
PydanticOutputParserAIMessagePydantic 对象复杂结构化输出

四、RunnableLambda:把自定义函数塞进链里

有时候你想在链的中间插入一步自定义逻辑——比如对文本做预处理、过滤敏感词、记录日志。RunnableLambda 可以把任何 Python 函数变成一个"可拼接的组件":

4.1 基础用法

from langchain.schema.runnable import RunnableLambda
from langchain.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain.schema.output_parser import StrOutputParser

# 定义一个普通函数
def to_uppercase(text: str) -> str:
    return text.upper()

# 包装成 Runnable
uppercase_runnable = RunnableLambda(to_uppercase)

# 塞进链里
prompt = ChatPromptTemplate.from_template("解释一下:{topic}")
llm = ChatOpenAI(model="gpt-4o-mini")

chain = prompt | llm | StrOutputParser() | uppercase_runnable

result = chain.invoke({"topic": "Python"})
print(result)  # 全部大写输出

4.2 实战:给输出加个"水印"

from langchain.schema.runnable import RunnableLambda

def add_watermark(text: str) -> str:
    return text + "\n\n---\n由 AI 生成,仅供参考"

chain = prompt | llm | StrOutputParser() | RunnableLambda(add_watermark)

result = chain.invoke({"topic": "机器学习"})
# 输出末尾自动加上水印

4.3 多步自定义处理

from langchain.schema.runnable import RunnableLambda

def remove_think_tags(text: str) -> str:
    """去掉模型的 思考过程 标签"""
    import re
    return re.sub(r'思考过程.*?/思考过程', '', text, flags=re.DOTALL)

def trim_whitespace(text: str) -> str:
    return text.strip()

# 串成处理链
cleanup_chain = (
    RunnableLambda(remove_think_tags)
    | RunnableLambda(trim_whitespace)
)

# 挂在主链后面
full_chain = prompt | llm | StrOutputParser() | cleanup_chain

RunnableLambda 的本质:把"链外的逻辑"变成"链内的组件"。 任何 Python 函数,只要输入输出类型对得上,就能无缝嵌入流水线。


五、RunnablePassthrough:数据"直通"与"分流"

RunnablePassthrough 是一个看似简单但极其有用的组件——它原样传递数据,不做任何修改。听起来没用?它的真正威力在于数据分流和并行。

5.1 基础:数据直通

from langchain.schema.runnable import RunnablePassthrough

passthrough = RunnablePassthrough()
result = passthrough.invoke("hello")
print(result)  # "hello" —— 原样通过

5.2 核心用法:数据分流

RAG 场景中最常见的模式——一个问题进来,同时传给检索器和提示词模板:

from langchain.schema.runnable import RunnablePassthrough, RunnableLambda
from langchain.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain.schema.output_parser import StrOutputParser

# 模拟检索器
def retrieve_docs(query: str) -> str:
    return "相关文档内容:LangChain 是一个 LLM 应用开发框架..."

# 用 RunnablePassthrough 做数据分流
chain = (
    {
        "context": RunnableLambda(retrieve_docs),  # 走检索
        "question": RunnablePassthrough(),          # 原样直传
    }
    | ChatPromptTemplate.from_template("根据以下资料回答问题:\n{context}\n\n问题:{question}")
    | ChatOpenAI(model="gpt-4o-mini")
    | StrOutputParser()
)

result = chain.invoke("LangChain 是什么?")
print(result)

5.3 数据流图解

输入:"LangChain 是什么?"
        │
        ├──→ retrieve_docs() ──→ "相关文档内容:..."
        │                              │
        │                              ├──→ {context: "相关文档...", question: "LangChain 是什么?"}
        │                              │              │
        └──→ RunnablePassthrough() ────┘              │
                                                      ▼
                                               提示词模板 → 模型 → 输出

RunnablePassthrough 的精髓:它让你可以"复制"一份原始数据,同时保留原始数据继续往下传。 这在需要把同一份数据同时喂给多个组件时非常有用。


六、RunnableParallel:让多个任务同时跑

有些场景下,你需要同时调用多个模型或执行多个操作,然后汇总结果。RunnableParallel 就是做这个的——并行执行,统一汇总。

6.1 基础用法

from langchain.schema.runnable import RunnableParallel, RunnableLambda

def analyze_sentiment(text: str) -> str:
    return "正面"

def extract_keywords(text: str) -> str:
    return "Python, 框架, 大模型"

# 并行执行两个分析任务
parallel_chain = RunnableParallel(
    sentiment=RunnableLambda(analyze_sentiment),
    keywords=RunnableLambda(extract_keywords),
)

result = parallel_chain.invoke("LangChain 是一个优秀的 Python 大模型框架")
print(result)
# {'sentiment': '正面', 'keywords': 'Python, 框架, 大模型'}

6.2 实战:同时调用两个模型,对比结果

from langchain.schema.runnable import RunnableParallel
from langchain.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain.schema.output_parser import StrOutputParser

prompt = ChatPromptTemplate.from_template("用一句话解释:{concept}")

# 用两个不同的模型
gpt4_chain = prompt | ChatOpenAI(model="gpt-4o-mini") | StrOutputParser()
qwen_chain = prompt | ChatOpenAI(
    model="qwen-plus",
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
    api_key="your-key"
) | StrOutputParser()

# 并行调用
compare_chain = RunnableParallel(
    gpt4=gpt4_chain,
    qwen=qwen_chain,
)

result = compare_chain.invoke({"concept": "递归"})
print(f"GPT-4: {result['gpt4']}")
print(f"通义千问: {result['qwen']}")

七、Chain 的调试:看清每一步发生了什么

链越长,调试越难。LangChain 提供了几个调试利器:

7.1 全局调试开关

import langchain
langchain.debug = True  # 开启全局调试

# 之后每条链的每次调用都会打印详细的中间过程
chain.invoke({"topic": "AI"})

输出会显示每一步的输入、输出、耗时等详细信息。

7.2 单条链调试:用 RunnableLambda 打印中间结果

from langchain.schema.runnable import RunnableLambda

def debug_print(data):
    print(f"[DEBUG] 当前数据: {data}")
    return data  # 原样返回,不影响链

chain = (
    prompt
    | llm
    | RunnableLambda(debug_print)  # 插入调试节点
    | StrOutputParser()
)

7.3 分步执行:手动检查每一步

# 链 = 分步执行
prompt_value = prompt.invoke({"topic": "AI"})
print(f"提示词: {prompt_value}")

llm_result = llm.invoke(prompt_value)
print(f"模型输出: {llm_result}")

final = StrOutputParser().invoke(llm_result)
print(f"最终结果: {final}")

调试技巧:复杂链先分段跑通,再拼成一条完整链。 一段一段验证,比直接调一整条链然后猜哪里出错,效率高十倍。


八、总结

组件作用一句话
管道符 |拼接组件LangChain 的"胶水",数据自动流转
Runnable 接口统一调用规范invoke/stream/batch/ainvoke 四种方式全支持
StrOutputParser提取文本把消息对象变成人话字符串
JsonOutputParser提取 JSON让模型输出结构化数据
RunnableLambda自定义函数入链任何 Python 函数秒变链内组件
RunnablePassthrough数据直通复制数据、分流,RAG 的标配
RunnableParallel并行执行多个任务同时跑,结果统一汇总

Chain 的精髓就四个字:组合优于继承。 你不用写一个"翻译+摘要+格式化"的大类,而是把三个小功能串起来——每个小功能独立可测,组合起来又能完成复杂任务。这就是 LangChain 最核心的设计哲学。


参考资料

更多推荐