底层拆解:LangChain的“Runnable”协议到底在搞什么
LangChain最核心的东西,不是Chain,不是Agent,而是那个Runnable接口。所有组件都实现这个接口,然后就能用|运算符串起来。这是它的设计精髓。你可以把任何东西变成管道的一节:一个prompt模板,一个LLM调用,一个输出解析器,甚至一个普通函数。这就像水暖工用的PVC管接头,口径是统一的,但里面流的什么你随便换。问题是,当管道多了,漏水了你就不知道该查哪一段。
从物理层看,LangChain的LCEL(LangChain Expression Language)其实就是用Python的__or__魔术方法,把两个对象包装成一个新的Runnable。这个新对象执行时,会先运行左边,把结果传给右边。看起来挺自然,但我用cProfile跑过一个简单链,内部函数调用次数是手写代码的20倍。为什么?因为每次执行要经过invoke、arun、_call、回调管理器、事件系统……一层套一层。
比如你想让模型输出JSON。传统方式,你写一个prompt,调用openai.ChatCompletion.create,然后手动json.loads。LangChain的方式是:
prompt = PromptTemplate.from_template('输出关于{topic}的JSON')
chain = prompt | llm | JsonOutputParser()
result = chain.invoke({'topic': 'AI'})
看着简洁,但每个|都会生成一个RunnableSequence对象,每个对象都有自己的一套invoke逻辑。更糟的是,如果你启用了langchain.callbacks,每个节点还会触发一堆回调,这些回调又通过wandb或langsmith传输。在开发环境里这没什么,生产环境里那就是白花花的银子和毫秒。

那么,LangChain的不可替代性在哪?我个人觉得,在于它对异步和流式的统一抽象。手写代码要同时处理sync和async两个版本,LangChain一个astream就能搞定。这点是它值得留下的理由。
性能实测:一点八秒和三点七秒的差距
我拿一个真实业务场景做了压测。场景是:用户问一个问题,Agent需要先调一个搜索工具,再调一个计算工具,最后汇总回答。我用两种方式实现:一种是LangChain的AgentExecutor,一种是手写Python逻辑,直接调用LLM和API。两种方式使用同一个GPT-4模型,temperature=0,工具调用次数相同。
输入同样10个问题,取平均耗时和token消耗,结果如下:
手写代码:平均1.86秒,每次任务约消耗2,300个token(含工具返回)。
LangChain Agent:平均3.72秒,每次任务约消耗4,050个token。
多了整整一倍!我打开trace看了,原因有三。第一,LangChain的AgentExecutor实现了ReAct策略,每轮都要模型先输出“Thought/Action”再解析,然后执行工具,再生成“Observation”,整个过程至少要两次LLM调用。第二,它会把整个中间过程都塞进下一轮Prompt,导致Prompt膨胀。第三,它内部用了一系列回调来记录状态,每次调用都会触发on_llm_start、on_llm_end等事件,这些回调本身也是时间消耗。
我还单独测了Memory模块。在连续5轮对话中,用ConversationBufferMemory,第5轮的Prompt比第1轮膨胀了3.2倍;而手写滑窗保持最近2轮只有原来的1.4倍。如果你用默认配置做长期对话,token成本会指数级上升。

不过,我得说句公道话。这3.7秒对于大多数非实时场景,比如客服工单分析,完全没毛病。但如果你是做实时语音交互,那这延迟就是致命的。所以,用不用LangChain,先测再判,别盲从。
落地避坑:三个我踩过的坑和填坑方案

坑一:盲目相信Agent能搞定一切。 我最初让Agent做一个简单的数据库查询,它居然试图调用日历工具。为什么?因为默认prompt里给了太多工具示例,模型误判了。解决方案:把工具数量控制在三个以内,并且每个工具的描述写清楚,用“当用户需要X时,才调用Y”这种条件式描述。另外,一定要设置max_iterations,我设了3,避免无限循环。我试过不设,结果这家伙自己跟自己聊了十几轮,最后token烧爆了。
坑二:输出解析器太脆弱。 让模型输出JSON,LangChain的PydanticOutputParser会很严格,稍微多一个逗号就报错。我试过在模型前面加一个“二次修正”链,虽然能解决,但成本翻了一倍。后来我干脆不用它的解析器,而是自己在prompt里强制要求“只输出JSON,不要解释”,然后用json.loads加上异常重试一次。简单粗暴,但有效。记住:框架的解析器只是辅助,业务的鲁棒性得自己兜底。
坑三:内存爆炸。 之前提到,Chain的Memory会把所有历史都存下来。我跑了十几轮对话,内存直线上升,因为ConversationBufferMemory默认无限制。解决方案:用ConversationSummaryMemory,它会定期总结历史,但注意总结也有token开销。更好的办法是:只保留最近N轮,然后用一个向量存储做长期记忆,按需检索。我后来直接用Redis存最近10轮,彻底绕开了LangChain的Memory模块。
这些坑,官方文档其实都写过,但它们藏在一堆API参考里,你不踩一遍根本记不住。我的最佳实践路径是:先手写原语,再逐步替换成LangChain组件,保证每一步都能回滚。千万别一上来就搭一个完整的Agent。
最后说一句,LangChain本身不是银弹,它更像一个工具箱。工具用得对不对,全看你会不会。我现在用它,只挑自己需要的部分,比如LCEL和回调系统,至于其他的……呵呵,能不用就不用。