LangChain 中 Runnable 与 Middleware 的定位及选型问题 #106
Replies: 1 comment
|
感谢提问。这个问题把 Runnable、Middleware、LangGraph 和 Deep Agents 这几个容易混在一起的层次问到了一起。 直接回答你关心的三点:
Runnable、LangGraph 和 Agent 的关系Runnable 是统一执行接口,提供 LangGraph 负责把执行单元编排成图。图里的节点既可以放 Runnable,也可以放普通 Python 函数: from langchain_core.runnables import RunnableLambda
retrieve_node = RunnableLambda(
lambda state: {
"docs": retriever.invoke(state["question"])
}
)
def format_node(state):
return {
"context": "\n\n".join(
doc.page_content for doc in state["docs"]
)
}
builder.add_node("retrieve", retrieve_node) # Runnable
builder.add_node("format", format_node) # 普通 Python 函数LangGraph 经过 LangChain 的 几层关系可以这样看:
所以 Middleware 不会替代 Runnable,它们本来就不在同一层。Runnable API;LangGraph 官方说明 Middleware 负责什么Middleware 管的是“Agent 运行到这里时,需要额外做什么”。比如:
同一段逻辑有时既能写成 Runnable,也能放进 Middleware,主要看它想表达什么。 比如“先检索,再生成报告”是明确的业务步骤,用 Runnable 或 LangGraph 更直观;“每次模型调用失败后重试两次”是 Agent 的统一行为,用 Middleware 更合适。 LangChain 现在的预制 Middleware 已经覆盖了大部分常见需求,包括重试与降级、上下文管理、HITL、调用限制、To-do、工具选择、Filesystem、File Search、Subagent、Shell、Rubric 等。Filesystem 和 Subagent 也已经是标准预制能力。常规 Agent 先用官方预制;缺少业务规则再写自定义 Middleware;标准 Agent Loop 表达不了时,再下沉到 LangGraph。官方预制 Middleware 列表 Deep Agents 现在解决什么问题Deep Agents 的价值不在于独占某几个 Middleware,而在于按深度任务的需要,把一批能力装配好了。 研究或编码 Agent 往往要同时处理任务规划、文件系统、上下文压缩、Skills、子 Agent、长期记忆,以及配合这些工具的系统提示词。只用 LangChain 也能搭,但需要自己选择组件、安排 Middleware 顺序、准备 Backend,再处理这些能力之间的配合。 Deep Agents 已经做了这层装配,更适合执行时间长、步骤多、中间产物也多的任务。普通问答或简单工具调用没必要直接上 Deep Agents, Deep Agents 中的部分 Middleware 也可以单独拿出来给普通 LangChain Agent 使用。Skills 就是一个典型例子:如果只想增加 Skills,可以使用 从选型上看,我会按这个顺序来:
这三者经常会叠在一起,因为 LangChain 和 Deep Agents 底层本来就运行在 LangGraph 上。官方产品分层说明 回到 RAG 这个例子RAG 放在哪里,要看检索在流程中扮演什么角色。 1. 每次都要先检索如果每次生成前都必须检索,我更倾向于把 RAG 放在 Agent 外部,用 Runnable 把依赖关系写清楚: from langchain_core.runnables import RunnableLambda
def prepare_agent_input(question: str):
docs = retriever.invoke(question)
context = "\n\n".join(doc.page_content for doc in docs)
return {
"messages": [{
"role": "user",
"content": f"参考资料:\n{context}\n\n问题:{question}",
}]
}
rag_agent = RunnableLambda(prepare_agent_input) | agent
result = rag_agent.invoke("Runnable 和 Middleware 有什么区别?")这里的流程很明确:先检索,再调用 Agent。Retriever 可以单独测试、缓存,也能被其他应用复用。这属于 2-Step RAG。 2. 检索是 Agent 的启动上下文如果检索只属于某个 Agent,是它每次启动时都要加载的上下文,放进 有一点容易忽略: 3. 由模型判断要不要检索如果要让模型自己判断是否检索,把 Retriever 做成 Tool,交给 4. RAG 自己就有分支和循环如果检索后还要评估文档相关性,结果不好时改写问题并重新检索,RAG 本身已经是一套工作流。把这些逻辑都藏进 Middleware 会比较绕,直接用 LangGraph 更清楚。 LangChain 官方的 Custom Agentic RAG 教程 就是这套流程:
这里的条件路由和循环直接写在图里。图经过 5. 基于 Deep Agents 构建 RAG如果 RAG 只是“模型按需调用一次 Retriever”,普通 LangChain Agent 已经够用。Deep Agents 更适合检索之外还要做任务规划、并行调研、保存中间材料或汇总长报告的场景。实现方式并不特殊,仍然是把 Retriever 包成 Tool,再交给 from langchain.tools import tool
from deepagents import create_deep_agent
@tool
def retrieve_knowledge(query: str) -> str:
"""检索内部知识库,返回相关片段及来源。"""
docs = retriever.invoke(query)
return "\n\n".join(
f"[{i}] {doc.page_content}\n"
f"source: {doc.metadata.get('source', 'unknown')}"
for i, doc in enumerate(docs, start=1)
)
research_subagent = {
"name": "knowledge-researcher",
"description": "检索知识库并整理有来源的材料",
"system_prompt": "先检索再整理,保留来源编号,不要补写资料中没有的事实。",
"tools": [retrieve_knowledge],
}
agent = create_deep_agent(
model="openai:gpt-5.4",
tools=[retrieve_knowledge],
subagents=[research_subagent],
system_prompt=(
"回答知识库问题时先检索;复杂问题拆分后交给子 Agent,"
"最后根据检索结果合并答案,并保留来源编号。"
),
)这时主 Agent 可以先列计划,把不同检索主题交给子 Agent,再借助文件系统保存和合并中间材料。官方的 Deep Research 教程 展示了相同的组织方式,只是示例里的检索源是 Web Search;换成向量库 Retriever、SQL、知识图谱或企业搜索 API,整体结构不变。 把 Deep Agents 也算进来,实际可以这样选:
实际项目里,我会先把 Retriever 保持成独立组件,再根据流程复杂度选择接法。这样以后从固定 RAG 改成 Agentic RAG,或者继续升级成自定义 LangGraph,都不会把检索逻辑绑死在某一层。 |
Uh oh!
There was an error while loading. Please reload this page.
目前 LangChain 提供了多种内置中间件,也支持自定义中间件。部分过去使用 Runnable 实现的流程,现在似乎也可以通过 Middleware 完成。
例如,若 Agent 执行前必须进行 RAG 检索,过去可以将 RAG 封装为 Runnable,通过链式调用接入流程;现在也可以将该逻辑放入 before_agent 中间件。
想请教:
All reactions