
作为一个从老版LLMChain一路用过来的开发者我太理解初学LangChain时那种“同一个功能为什么有人几行代码写完我却要写上百行”的困惑了。这一切的根源都在于LangChain存在两层API设计低阶APILangChain Core / LangChain Community和高阶APILangChain Expression Language、封装好的Chain、以及LangGraph这种新一代编排模型。理解这两层API的区别基本上就等于拿到了LangChain的“使用地图”。这篇我打算掰开揉碎聊聊我对这两层API的理解它们分别是什么、各自解决什么场景、实际写代码时怎么选、以及我从低阶切到高阶再回到低阶、最终形成“混合开发”习惯的过程。内容基于我常用的LangChain 0.3.x版本如果你用的是老版本个别细节可能需要微调但设计思想完全通用。1. 为什么LangChain要拆成“低阶”和“高阶”两层API先说一个生活化的类比。我把高阶API想象成“自动驾驶”低阶API想象成“手动挡驾驶”。高阶API帮你把“踩油门、打方向盘”这类常规动作预设好了。你要做的只是设定目的地prompt、启动引擎invoke。像RetrievalQA、create_sql_agent这类集成就是把“查数据库、推理、组装结果”一整套流程打包好。开高速稳、省心、速度快你不需要知道发动机原理。低阶API则把这些操作全部摊开。你能看到每个齿轮BaseMessage、每根传动轴ChatPromptTemplate、每次换挡逻辑RunnableLambda。你可以中途改变速度逻辑甚至自己造一个变速箱自定义CallbackHandler。设计这么两套东西LangChain团队真正的逻辑是用高阶满足80%的标准化需求用低阶承接剩下20%的定制化需求同时让低阶成为高阶的地基。举个例子ChatOpenAI这个类既是底层的东西它实现了BaseChatModel也是高层API的组成部分。你用LangChain的“表达式语言LCEL”即|管道符连接的Runnable本质是低阶组件的抽象接口。这也就意味着——两个阶层并没有泾渭分明的界限只是同一个工具箱里的不同扳手。因此正确的理解方式不是“哪个更好”而是“你正处在哪个开发阶段对应使用哪一层”。1.1 高阶API的蓝图从“快速演示”到“复杂Agent”LangChain的高阶API我粗分三层来看。第一层预构建的“经典Chain”像RetrievalQA、ConversationRetrievalChain、SQLDatabaseChain。优点是你调用一行它就给你完整链路适合快速跑通POC。缺点是黑盒很重你想改内部细节往往发现它又新起了另一个分支Chain嵌套地狱在这里等着你。第二层面向模块的“表达语言LCEL”核心是Runnable协议。它像极了你拼接水管prompt | model | parser。什么样的东西都实现了Runnable用一个|连接后返回新的Runnable。这是我从“老的链式语法”迁移后的主力开发方式。它既可以极简两三行跑通也可以逐步拆开每个节点用函数控制信息密度与可读性都极高。第三层新一代Agent编排——LangGraphLangGraph严格说不是“API”那么轻它是一个完整的编排框架。官方几个高阶Agent抽象如create_react_agent跑在它之上。如果你要写复杂的循环、分支、人机交互、记忆管理LangGraph是官方推荐的长线方案。在这个框架里你能同时看到低阶的细粒度控制State更新、节点条件跳转又有高阶的图级抽象。1.2 低阶API的骨架所有概念的“原材料”低阶API并不复杂它几乎是“某种消息在某种过程中如何流转”的完整定义。核心由三类素材组成Message数据模型BaseMessage及其子类SystemMessage、HumanMessage、AIMessage、ToolMessage。这是所有聊天的“砖块”。核心可运行组件PromptTemplate、ChatModelLLM、OutputParser、Retriever。运行与监控机制Runnable调用链、Callbacks事件回调、MessageHistory记忆。低阶让你清楚地面对这些“砖块”的每一次移动与变化。比如你会自定义一个RunnableLambda来获取中间状态也会自己维护ChatMessageHistory去追加消息而不是直接用某个封装好的带记忆Chain。我的经验接触LangChain的第一步往往从高阶官方文档的Quick Start入手但是当项目复杂度起来后多工具选择、异常重试、长链路状态恢复只能在低阶层面才能得到充分的活性。把低阶作为底盘思想是写LangChain项目走向专业的必经之路。2. 低阶API的核心组件拆解与实操要点这部分是我早期踩坑踩得最深的区域。低阶不意味着简单反而是因为知识粒度细组合起来非常灵活但也容易出错。2.1 消息类BaseMessage与角色状态在ChatModel对话类模型的语境里一切输入输出都被抽象成BaseMessage子类。你传入的是消息列表返回的也是消息。它们之间的关系是SystemMessage设定模型人格、回答边界。HumanMessage用户的输入包括普通文本、图片、音视频等。AIMessage模型的回答内容同时工具调用的请求也会挂在它的tool_calls属性上。ToolMessage工具执行后的返回结果必须与对应的tool_call_id关联。我在低阶实践早期遇到的最大坑就是“为什么这个记忆代码存了上一轮对话但模型完全不记得”因为我只是把历史作为字符串拼接进大模型没有区分AIMessage和HumanMessage。后来我改为用ChatMessageHistory维护消息数组严格按照“用户发一句AI回一句再轮到用户”的顺序传回模型对话连贯性马上就好了。注意在低阶中务必明确一点对于多模态输入把图片用HumanMessage(content[{type:text,text:...},{type:image_url,image_url:{...}}])塞进去。这是很多初学者把低阶当高阶用照搬prompt | model就报格式错误的原因——你不清楚不同模态在“消息类”里是如何表示的。2.2 模板类PromptTemplate与ChatPromptTemplate的边界PromptTemplate字符串模板适合文本补全类模型如旧版text-davinci-003但今天使用Chat模型的主力是ChatPromptTemplate它接受一系列消息模板且模板里可以使用变量。from langchain_core.prompts import ChatPromptTemplate prompt ChatPromptTemplate.from_messages([ (system, 你是一个{role}请用{style}的风格回答用户的问题。), (human, {question}), ])这里有一个低阶实操细节模板消息使用from_messages时传进的是可变参数模板内联变量是隐藏在字符串里的。如果我想对输出做校验重试就需要在低阶层面自行注入新消息。比如from langchain_core.messages import AIMessage, HumanMessage partial_prompt prompt.partial(role资深顾问, style简洁专业)partial是管道式构建中经常用到的一环它相当于预填部分变量其他变量在真正调用时再一次性传入。2.3 Model IO从底层理解“为什么会有输入输出解析器”模型拿到的是字符串或消息。你希望程序拿到的是结构化数据dict、list、Pydantic对象这就轮到OutputParser上场。低阶API中的PydanticOutputParser是我用得最多的。from langchain_core.output_parsers import PydanticOutputParser from pydantic import BaseModel, Field class Joke(BaseModel): setup: str Field(description问题的设定) punchline: str Field(description问题的爆笑回答) parser PydanticOutputParser(pydantic_objectJoke) format_instructions parser.get_format_instructions()这里有个关键点模型输出必须严格遵循指令给出的JSON格式。为了保证成功率往往需要在低阶增加“校验爬虫”逻辑把模型输出丢给parser.parse失败就提示模型重新格式化输出甚至让模型自己描述哪里错了然后把这套描述作为新提示继续调用模型。低阶的Model I/O中你与“失败”是直接面对面的。这正是它的价值大部分高阶失败是因为模型返回格式错误但低阶可以让你直接捕获异常在模型层做重试或降级。2.4 回调机制低阶世界里的“日志、追踪与拦截”我接LangChain项目的时候第一件事通常是装LangSmith、Langfuse或至少打开verboseTrue这背后都是回调系统在工作。你可以在自定义BaseCallbackHandler里捕获on_llm_start拿到提示信息查看系统提示到底写了什么。on_llm_new_token做流式输出的实时交互。on_chain_end查看整条链的最终输出。一个真实经验我曾经遇到“为什么某次模型调用老是不输出”排查时把回调打开发现某个Runnable组件没有正确将中间步骤传给下一步才意识到是变量命名和RunnableLambda的input传递问题。回调代码参考from langchain_core.callbacks import BaseCallbackHandler class MyCallback(BaseCallbackHandler): def on_llm_start(self, serialized, prompts, **kwargs): print(提示词, prompts[0][:100]) def on_llm_end(self, response, **kwargs): print(生成内容, response.generations[0][0].text[:100])回调体系是低阶API最值钱的隐藏武器。很多高阶链的调试手段就是靠这份事件流了解挂载方式之后就等于拥有了对全局过程的洞察力。3. 高阶API的全景拆解与核心用法如果说低阶是“造车”那高阶就是“开车”。这里我讲两种主流的高阶用法LCEL管道和经典Agent封装。3.1 LCEL为什么一个竖线“|”就能串起整个流程LangChain的LCEL其实是低阶实现中衍生出的“高阶表达”。它的设计动机只有一句话让组件像UNIX管道一样组合。每个组件都实现Runnable接口invoke/batch/stream/ainvoke通过|重载操作符把左边输出交给右边输入。from langchain_openai import ChatOpenAI from langchain_core.output_parsers import StrOutputParser llm ChatOpenAI(modelgpt-4o-mini, temperature0.7) chain ( {question: lambda x: x[question]} | ChatPromptTemplate.from_template(请回答{question}) | llm | StrOutputParser() )这种链的好处是你可以调用.stream()逐token打印可以调用.batch()并行处理多个问题也可以把某个环节替换成自定义函数而不破坏链的整体结构比如在中间插入RunnableLambda。它不算传统意义上的“高阶黑盒”却比低阶挨个调用优雅得多。这里也有人会问到“低阶API和LCEL的区别是什么”核心差异在于低阶API强调组件的可组合性和内部数据结构LCEL是这个组合性的“最高阶表现形式”什么都作为Runnable赋能。3.2 经典封装Chain为何我慎重推荐使用像load_qa_chain、RetrievalQA这些封装对于一小时做出Demo来说是神器。但放在生产环境我吃过好几次苦头。我以前做过一个内部知识库问答系统用RetrievalQA.from_chain_type(llm, chain_typestuff, retrieverretriever)轻轻松松跑通。但后来发现以下致命问题“stuff”类型把所有文档塞进一个提示词里上下文稍长就超限“map_reduce”类型对每段分别调用LLM成本高且汇总时细节会被“压缩”你无法方便地在中间插入“判定文档相关性”“给检索结果打分”等自定义逻辑。在这个项目中后期我改成纯LCEL手写链把检索交给一个调用里命令行人为控制prompt拼接——代码长了但意图变得清晰维护也不再是噩梦。我的建议场景推荐方式理由快速demo、内部工具经典Chain如RetrievalQA两三行搞定效果直观生产级RAG、需要检索优化LCEL自行组装逻辑清晰可控性高每步可测试多工具循环、决策分支LangGraph或低阶工具循环需要状态管理与循环能力简单结构化输出高阶PydanticOutputParser搭配LCEL快速、可验证3.3 Agent高阶封装从create_react_agent看到的“框架之力”Agent曾经是LangChain最玄学的部分。当前比较稳妥的高阶玩法是langgraph.prebuilt.create_react_agent它把“ReAct循环”内置了模型决定调用哪个工具工具返回结果模型再决定下一步直到输出最终答案。from langchain_openai import ChatOpenAI from langgraph.prebuilt import create_react_agent def get_weather(city: str) - str: 查询指定城市的天气 return f{city}今天晴24度适合出行~ llm ChatOpenAI(modelgpt-4o-mini, temperature0) agent create_react_agent(llm, tools[get_weather]) result agent.invoke({messages: [{role: user, content: 北京今天适合出门吗}]})这个抽象的地基是中国人口中的“低阶”——你看到的是get_weather包装成了Tool、函数通过docstring生成参数schema、模型与工具之间的消息被自动管理。不了解低阶你会以为它很“魔法”了解低阶你会明白只是“消息协议函数调用约束”的自动化。4. 实操案例用同一需求对比低阶手写与高阶封装讲到这里还是上代码最直观。我用一个“从网络/数据库获取信息并让模型总结”的任务分别用低阶和LCEL实现一遍。4.1 低阶API完整手写工具调用循环这段代码展示了真正“低阶到每个节点”的控制每一步我自己掌控。import json from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, ToolMessage from langchain_core.tools import tool from langchain_openai import ChatOpenAI llm ChatOpenAI(modelgpt-4o-mini, temperature0) tool def get_stock_price(symbol: str) - str: 获取指定股票代码的当前价格 # 这里换成真实行情接口 price_map {AAPL: 190.20, GOOGL: 142.50} return price_map.get(symbol.upper(), 未知股票) tools [get_stock_price] llm_with_tools llm.bind_tools(tools) messages [ SystemMessage(content你是一个财经助手。获取实时数据后回答用户问题。), HumanMessage(content苹果公司当前的股价是多少) ] # 第一轮模型决定是否调用工具 response llm_with_tools.invoke(messages) messages.append(response) # 如果模型产生工具调用执行并回填 if response.tool_calls: for tool_call in response.tool_calls: tool_name tool_call[name] tool_args tool_call[args] tool_output get_stock_price.invoke(tool_args) messages.append(ToolMessage(contenttool_output, tool_call_idtool_call[id])) # 第二轮把工具结果交给模型让模型整理最终话术 final_response llm_with_tools.invoke(messages) print(final_response.content)麻烦吗麻烦。但你能做到“精确到每一轮模型说什么、工具返回什么、消息列表如何追加”。如果我要在低阶增加“连续调用多个工具”“工具出错自动修正参数”“超时重试”这个循环可以被我改得面目全非——这正是定制化需求的沃土。4.2 高阶LCEL同样功能写成管道from langchain_core.runnables import RunnableLambda, RunnablePassthrough from langchain_openai import ChatOpenAI from langchain_core.tools import tool from langchain_core.output_parsers import StrOutputParser tool def get_stock_price(symbol: str) - str: 获取指定股票代码的当前价格 price_map {AAPL: 190.20, GOOGL: 142.50} return price_map.get(symbol.upper(), 未知股票) llm ChatOpenAI(modelgpt-4o-mini, temperature0).bind_tools([get_stock_price]) def route(state): 判断是否需要继续调用工具 last_message state[messages][-1] if last_message.tool_calls: # 这里用低阶逻辑处理工具调用并追加消息 for tc in last_message.tool_calls: result get_stock_price.invoke(tc[args]) state[messages].append(...) # 构造ToolMessage return {messages: state[messages]} return {messages: state[messages]} chain ( RunnablePassthrough.assign(messageslambda x: [HumanMessage(contentx[question])]) | RunnableLambda(lambda state: llm.invoke(state[messages])) | RunnableLambda(route) | RunnableLambda(lambda state: state[messages][-1].content) | StrOutputParser() ) result chain.invoke({question: 苹果公司当前的股价是多少}) print(result)从这个对比里你能直觉感受到什么LCEL加了代码结构上的整洁但没有减少底层概念的复杂度。它让中间节点以可复用、可组合的方式呈现适合构建稳定标准化的流程低阶则适合深挖分支逻辑和动态行为。两者并不互斥而是在我做复杂Agent时交替使用。5. 常见问题与排查经验实录写了这几年LangChain也帮不少群友排查过问题。下面这些是高频雷区。5.1 “为什么我用了高阶API但总是跨域/返回格式报错”常见的坑就是模型返回JSON格式不标准。高阶层会把输出交给PydanticOutputParser但底层调用的“先生成文本再解析”不可依赖。我的建议是务必将模型返回强制成JSON模式如response_format{type:json_object}然后用低阶RunnableLambda做一次解析重试解析失败就把错误反馈重新丢回模型。from langchain_core.runnables import RunnableLambda def safe_parse(text: str): try: return json.loads(text) except Exception: # 返回占位等待后续逻辑重新调用 return {error: parse failed, raw: text} chain prompt | llm | StrOutputParser() | RunnableLambda(safe_parse)5.2 “工具调用结果没有进入上下文模型总在复读”这个我在3.x常见通常是因为ToolMessage的tool_call_id与AIMessage.tool_calls里的id不匹配。很多封装里也能遇到这个。排查时最有效的方法是打印messages数组看每个AIMessage的tool_calls是否关联唯一ID。5.3 “使用高阶Agent忘加回收站——陷入无限循环”我早期写create_react_agent遇到模型反复调用同一工具输出相同结果导致Token耗尽。解法是加“最大迭代次数”recursion_limit和“检测重复调用”的低阶回调。比如在工具里加逻辑if tool_name in previous_tool_set: return 该工具已调用过结果不变请直接根据已有信息回答。5.4 “高阶层调用了低阶API却不可调和”遇到链报错不是链的问题往往是提示词里“{context}”变量没有传全。我用LCEL时最容易错的就是invoke时传入键名和ChatPromptTemplate的变量名不一致。排错宝典第一条把入参字典调整为模板变量的并集。5.5 “版本升级API天翻地覆”LangChain版本更新快RetrievalQA在高版本中已标记deprecated不推荐使用未来会转向create_retrieval_chain、LCEL甚至LangGraph。提示如果项目长期维护建议锁定版本langchain、langchain-openai、langchain-community全部固定或在依赖文件中明确指定langchain0.3。6. 低阶与高阶的混用心得我从“选边站”变化为“按层治理”写这个标题其实我很想分享的最终结论不是“一定要用哪个”而是**“分清楚你在哪一层工作”**。对我来说比较稳定的开发策略已然形成先用低阶画核心循环。最初的Agent循环、工具调用、状态更新我在纸上先画出来消息如何流动哪些步骤需要动态分支哪些地方必须手动维护历史。这个阶段我只关心低阶的Message与Runnable不用封装。再用LCEL将稳定部分固化为Pipeline。比如“格式化用户输入 - 检索 - 组装Prompt - 调用模型 - 解析输出”这几个固定步骤用LCEL封装成一个函数。参数只需传必要的业务字段内部用RunnableLambda引用工具函数。最终用高阶Agent来承载复杂跳转但不迷信它的默认行为。create_react_agent这个API名字虽是“高阶”但它允许在内部塞低阶工具回调。控制它的关键仍在工具函数的可靠性、提示词质量与状态清理。比如说我之前写一个“研发团队周报分析机器人”周报数据源在Jira分析动作在模型。用低阶API手写了工具函数的每次调用、Jira接口筛选逻辑、多轮追问的状态保存但对外暴露时我用LCEL构建了三段清晰的链查询汇总、结构解析、格式化输出。这中间的“分层体验”是调试前期问题我用的是低阶的全量输出后期性能优化我用到了高阶的并行batch。低阶不是高阶的替代品而是高阶的“下一层”就像地基与房子一样。我觉得LangChain最值得称道的地方反而是它并没有强制你“非此即彼”它将组件统一抽象成Runnable把底层知识榨干后你完全可以在同一个文件里一会儿写低阶函数一会儿写管道链。这种混搭不是代码风格混乱而是合理利用每一层API本身的长处。最后分享一个小技巧无论你用哪一层都要时刻关注消息类型与变量注入。LangChain99%的运行报错都出在这两处。把握住它们低阶和高阶在手里便只是工具的差异不再有任何门槛。