LangChain 现在的主线是 LCEL——每个组件都是 Runnable,用管道符串起来,流式、批量、异步、重试这些能力顺着管道白送。但很多人还在用旧版写法,或者把链搭起来之后被几个运行期问题卡住。这篇把最常见的误区挑出来,一个个说清正确做法。
别再用旧版链类
LLMChain、SequentialChain、ConversationChain、initialize_agent 这些已经废弃。新代码统一走 LCEL 管道,或者用 create_tool_calling_agent 加 AgentExecutor。判断一篇教程要不要信,先看它是不是还在 import 这些旧类,是的话多半是过时的。
# 新写法:管道串起来
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
prompt = ChatPromptTemplate.from_messages([
("system", "你是严谨的技术助手"),
("human", "{question}"),
])
chain = prompt | llm | StrOutputParser()
输入键要对得上
LCEL 最常见的运行期报错是键不匹配:上一个 runnable 输出的是 {"text": ...},下一个 prompt 却声明要 {summary}。这个错误定义链的时候不报,只有真正跑起来才炸。
排查的办法是打印中间数据流。用 RunnablePassthrough 把某一步的输出透传出来看一眼,或者 chain.get_graph().print_ascii() 把链结构画出来,看数据在哪一步断了。两个独立分支之间没有依赖,就用 RunnableParallel 并行跑,省掉串行的等待时间。
重试会放大成本
with_retry 是 Runnable 自带的重试,但默认它对任何异常都重试——包括你 prompt 写错导致的输出解析失败。一条注定失败的链,重试 3 次就是白烧 3 倍的钱。
正确做法是先打日志、确认失败原因,再决定要不要加重试。真正该加重试的是外部 API 的限流和超时,这类用带抖动退避的重试,再配 with_fallbacks 兜底到另一个模型。
robust = (
prompt
| llm.with_retry(stop_after_attempt=3, wait_exponential_jitter=True)
| parser
).with_fallbacks([fallback_chain])
记忆要按会话隔离
带记忆的链,最大的坑是串用户。RunnableWithMessageHistory 要按 session_id 隔离,多租户场景下忘了传 session_id,A 用户的历史就会漏给 B 用户。
from langchain_core.runnables.history import RunnableWithMessageHistory
with_history = RunnableWithMessageHistory(
chain,
get_session_history, # 按 session_id 返回对应历史
input_messages_key="question",
history_messages_key="history",
)
with_history.invoke(
{"question": "..."},
config={"configurable": {"session_id": "u-1001"}},
)
流式要自己攒全量
用 .stream() 逐块消费时,下游拿到的只是碎片。如果后面还要用完整字符串,得自己在循环里拼起来。做流式 UI 时,别依赖回调的触发顺序来改状态——回调顺序并不保证稳定,自己发结构化事件(比如 {"type":"token"}、{"type":"tool_start"})比依赖回调可靠。
结构化输出别手动解析
需要模型返回 JSON 或固定结构时,别让模型输出一段字符串再自己用正则去切。用输出解析器,把 schema 约束和格式说明一起绑进 prompt:
from langchain_core.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field
class Answer(BaseModel):
conclusion: str = Field(description="一句话结论")
confidence: int = Field(description="置信度 0-100")
parser = PydanticOutputParser(pydantic_object=Answer)
prompt = ChatPromptTemplate.from_messages([
("system", "按要求输出 JSON。
{format_instructions}"),
("human", "{question}"),
]).partial(format_instructions=parser.get_format_instructions())
chain = prompt | llm | parser
result = chain.invoke({"question": "..."}) # result 是 Answer 对象,不是字符串
手动解析的坑在于:模型偶尔会输出 JSON 前后带解释文字、或者字段名写错,正则一崩就是一整条链路失败。解析器把这些脏情况挡在结构外面,失败时还能配上重试兜底。
什么时候别用 LangChain
LangChain 不是默认选项。如果只是一次直接的模型调用——一个 prompt 进去、一个答案出来——用官方 SDK 更清楚,套一层链反而多了一堆要维护的抽象。它值得上的场景是:要串起三四个组件(检索、工具、记忆、解析)、要换 Provider 不重写、或者要统一的追踪和重试。等你的工具循环开始有状态、有分支、要循环了,再从 LCEL 升级到 LangGraph。判断线可以划得很简单:一个调用能说清的事用 SDK,三件以上的组件要协同才上 LangChain,出现循环和状态机就上 LangGraph。
LangChain 的正确用法概括成一句:能用管道就别手写编排,能靠 Runnable 自带的重试和兜底就别自己包 try/except,能交给 LangSmith 追踪就别靠猜。它是"集成",不是"智能"——把链收拾干净不会让模型变聪明,但会让出问题时你知道该查哪一环。