【Langchian】Runnable是什么?以及Langchian记忆管理的不同方式。

98 阅读8分钟

Runnable 是啥?

LangChain 的链(Chain)机制确实是通过高度模块化的设计来实现其强大功能的,而核心就在于它的抽象基类 RunnableRunnable 规定所有“可执行”对象的标准。让 LangChain 的零件(LLM、Prompt、Chain 等)统一操作,能单独跑,也能连成流水线。如果每个模块用自己的方式跑(llm.callprompt.generate),没法统一,连不起来。所以只要一个组件继承了 Runnable 并实现了对应的方法(比如 invoke),它就能被当作一个节点,自由地与其他节点连接起来组成链。实际开发中,LangChain 默认提供的节点确实不多,比如基本的提示模板(PromptTemplate)、语言模型调用(LLM)、输出解析器(OutputParser)等,这些是开箱即用的“标配”。真正发挥威力的往往是开发者自己定义的节点。

其实Coze 这种低代码拖拽平台的底层逻辑其实就是 LangChain 的链”,本质都是模块化 + 组合 ,只是表现形式不同:一个是代码驱动,一个是可视化驱动。

Runnable规定了什么

  • 标准化:invoke 单次执行,batch 批量,stream` 流式。
  • 组合化:用 | 连成链。(用 | 组成链,输出接输入。)
  • 扩展化:新模块只要继承 Runnable,就能加入。

invoke 是“攒完整再呈现”,流式输出是“边生成边展示”。两者本质都是自回归生成,但 invoke 隐藏了推理过程,流式输出暴露了每一步的动态上下文扩展,让用户实时感知模型的思考路径。

定义节点的过程其实也很直观。如果你手里有个普通的 Python 函数,想把它变成一个节点,直接用 RunnableLambda 包一下就行了。RunnableLambda 就像一个适配器,把函数封装成符合 Runnable 接口的组件,扔进链里就能用。比如你写了个函数来清洗文本数据,用 RunnableLambda 一包,它就能接在提示生成节点后面,处理完再交给语言模型,流水线就搭起来了。如果不是函数,比如你自己写了个类来处理复杂逻辑(比如带状态的计算、外部API调用),那就直接继承 Runnable 实现必要的接口方法,效果也是一样的。LangChain 提供了多种封装方式,像 RunnableParallel(并行执行)、RunnablePassthrough(直接传递输入)等,针对不同场景都有对应的工具,开发者可以根据需要挑合适的用。

Runnable的子类

既然Runnable 是个抽象基类,自然本身不干活,具体工作交给子类。它的家族分成几大类,按功能划分:

  • 直接子类:继承 Runnable,自己实现逻辑,干具体活。比如ChatOpenAIRunnablePassthrough
  • 封装子类:不直接继承,而是包装其他 Runnable,加新功能。比如RunnableWithMessageHistory
  • 组合子类多个 Runnable| 组成的新对象,本身也是 Runnable。比如Chain(像 prompt | llm)。

以下是 LangChain 中最常见的 Runnable 子类,按功能分类,附代码和关系说明。

模型类:比如ChatOpenAI就是直接子类,继承 Runnable,实现 invoke 调用 API。调用 OpenAI 模型生成回答。输入:字符串或消息列表。输出AIMessage

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(openai_api_key="your-api-key")
result = llm.invoke("你好")  # 输出:AIMessage(content="こんにちは")

提示词类:比如ChatPromptTemplate也是直接子类,继承 Runnable,实现 invoke 生成提示。 输入:字典(如 {"input": "你好"})。 输出:消息列表。

from langchain.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([("human", "{input}")])
result = prompt.invoke({"input": "你好"})  # 输出:[HumanMessage("你好")]

工具类:比如RunnablePassthrough也是直接子类,继承 Runnable。用于透传输入,可用 assign 加字段。 输入:任意。 输出:原输入或增强后的输入。 RunnableLambda也是直接子类,把普通函数变成 Runnable功能是包装自定义函数。 输入:任意。 输出:函数结果。

from langchain_core.runnables import RunnablePassthrough

passthrough = RunnablePassthrough.assign(extra=lambda x: "附加")
result = passthrough.invoke({"input": "你好"})  # 输出:{"input": "你好", "extra": "附加"}

--------------------------------------------------------------------------------------

from langchain_core.runnables import RunnableLambda

runnable = RunnableLambda(lambda x: x["input"] + "!")
result = runnable.invoke({"input": "你好"})  # 输出:"你好!"

输出类:比如 StrOutputParser,负责从 AIMessage 提取文本,也是直接子类,继承 Runnable,实现 invoke 解析输出。 输入AIMessage输出:字符串。

from langchain_core.output_parsers import StrOutputParser

parser = StrOutputParser()
result = parser.invoke(llm.invoke("你好"))  # 输出:"こんにちは"

历史类:比如RunnableWithMessageHistory,属于封装子类,包住其他 Runnable(如 llm),加历史功能 输入:字典 + 会话 ID。 输出:链的结果。

from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_community.chat_message_histories import SQLChatMessageHistory

def get_session_history(session_id):
    return SQLChatMessageHistory(session_id=session_id, connection_string="sqlite:///chat.db")

chain_with_history = RunnableWithMessageHistory(llm, get_session_history, input_messages_key="input")
result = chain_with_history.invoke("你好", config={"configurable": {"session_id": "user123"}})

组合类:比如Chain组合子类,由多个 Runnable 合成,它本身也是 Runnable 输入:第一个节点的输入。 输出:最后一个节点的输出。

chain = prompt | llm | parser
result = chain.invoke({"input": "你好"})  # 输出:"こんにちは"

Langchian的记忆实现策略

在 LangChain 中,链(Chain)默认是无状态的,这意味着每次运行都是独立的,无法自动记住之前的对话历史。比如用户说“我叫小明”,然后问“我是谁?”,如果没有额外的记忆机制,系统无法回答“你叫小明”,因为它不具备上下文记忆能力。要实现这种对话历史的保留,就需要引入记忆机制。而在具体实现时,你的部署方式和调用方式会直接影响是否需要自行处理记忆逻辑。

如果你是本地调用官方API,而不是本地部署模型,那么情况是这样的:官方API通常是由服务提供方(如xAI或其他平台)管理的,他们可能会在服务端内置一定的记忆管理机制,但这取决于API的具体设计。如果官方API已经提供了会话管理功能(比如通过会话ID或上下文参数来追踪对话历史),那么你在本地调用时就不需要额外操心记忆问题,只需按照API的规范传递相关参数即可。但如果API没有内置记忆功能,或者你希望自定义记忆行为(比如控制存储的内容或方式),那你还是得在本地代码中引入记忆机制。简单来说,只要API没有现成的上下文管理,你就得自己动手,而本地部署模型只是让这个问题变得更显而易见罢了。

反过来,如果你是本地部署模型(即模型完全运行在你的设备上),那么整个流程都在你的掌控之中,LangChain 的链默认无状态特性就会直接体现出来。这时,你必须自己实现记忆功能,比如通过 RunnablePassthrough.assignRunnableWithMessageHistory 这类工具。前者通过手动分配上下文来传递历史信息,适合简单的场景;后者则是更强大的方案,专门为对话历史管理设计,能更方便地集成消息记录。

方案 1:RunnablePassthrough.assign —— 手动输入增强

from langchain.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain.memory import ConversationBufferMemory

llm = ChatOpenAI(openai_api_key="your-api-key", model_name="gpt-3.5-turbo")
prompt = ChatPromptTemplate.from_messages([
    ("system", "你是助手,基于历史 {history} 回答"),
    ("human", "{input}")
])
parser = StrOutputParser()

memory = ConversationBufferMemory(return_messages=True)  # 内存存储
chain = (
    RunnablePassthrough.assign(history=lambda x: memory.load_memory_variables({})["chat_history"])
    | prompt
    | llm
    | parser
)

result = chain.invoke({"input": "我叫小明"})
memory.save_context({"input": "我叫小明"}, {"output": result})
print(chain.invoke({"input": "我叫啥?"}))  # 输出:你叫小明
  • RunnablePassthrough.assign
    • 作用:在链开头增强输入,添加 history 字段。
    • 实现:lambdaConversationBufferMemory (内存)加载历史,塞进输入字典。
    • 输入示例:{"input": "我叫啥?"}{"input": "我叫啥?", "history": [历史消息]}
  • ConversationBufferMemory
    • 存储:内存(RAM),程序运行期间有效。
    • 方法:load_memory_variables 读,save_context 写。
    • 限制:程序关闭后历史丢失。
  • 无状态链chain 本身不保存记忆,每次 invokeassign 临时提供。
  • 手动管理:需显式调用 memory.save_context 保存历史。
  • 存储位置:内存,非持久化。要持久化,需自己加逻辑(如存文件或数据库)。
  • 优点:简单,适合调试或单次会话。
  • 缺点:不自动持久化,长期使用需手动扩展。

方案 2:RunnableWithMessageHistory —— 自动历史管理

from langchain.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_community.chat_message_histories import SQLChatMessageHistory

llm = ChatOpenAI(openai_api_key="your-api-key", model_name="gpt-3.5-turbo")
prompt = ChatPromptTemplate.from_messages([
    ("system", "你是助手,基于历史回答"),
    MessagesPlaceholder(variable_name="chat_history"),
    ("human", "{input}")
])
parser = StrOutputParser()

chain = prompt | llm | parser  # 原链
def get_session_history(session_id):
    return SQLChatMessageHistory(session_id=session_id, connection_string="sqlite:///chat.db")

chain_with_history = RunnableWithMessageHistory(
    chain,
    get_session_history,
    input_messages_key="input",
    history_messages_key="chat_history"
)

result = chain_with_history.invoke(
    {"input": "我叫小明"},
    config={"configurable": {"session_id": "user123"}}
)
print(chain_with_history.invoke(
    {"input": "我叫啥?"},
    config={"configurable": {"session_id": "user123"}}
))  # 输出:你叫小明
  • RunnableWithMessageHistory
    • 作用:封装原链,自动加载和保存历史。
    • 实现:通过 get_session_history 获取历史对象,运行前加到输入,运行后存回。
    • 输入示例:{"input": "我叫啥?"}{"input": "我叫啥?", "chat_history": [历史消息]}
  • SQLChatMessageHistory
    • 存储:数据库(SQLite),持久化。
    • 机制:按 session_id 存取历史,程序重启仍可用。
  • 参数
    • input_messages_key:输入字段名。
    • history_messages_key:历史字段名,匹配 MessagesPlaceholder
  • 有状态链:封装后自动管理历史。
  • 持久化:存数据库,跨会话有效。
  • 自动化invoke 自带历史加载和保存。
  • 优点:省心,适合多用户、长期会话。
  • 缺点:需配置数据库,稍复杂。

  • RunnablePassthrough.assign

    • 类比:像给助手递张便签,告诉它“之前聊了啥”,但得自己记笔记。
    • 场景:快速测试、单次对话、手动控制历史。
    • 扩展:搭配 ConversationBufferMemory 可存内存,需手动持久化到文件或数据库。
  • RunnableWithMessageHistory

    • 类比:像助手自带账本,自动查记历史,你只管问。
    • 场景:生产环境、多会话应用、需要持久化。