首页 / 资讯中心 / 文章详情

Langchain和Langgraph实战:从链式调用到有状态工作流编排

Langchain和Langgraph实战:从链式调用到有状态工作流编排 ★ FEATURED ARTICLE
这两年做大模型应用最明显的一个感受是很多人卡在“调通了API”到“做出了产品”之间的这条沟上。市面上模型服务已经够多能力也够强但真正把模型接进业务流程、做成一个能稳定跑起来的应用中间涉及到的提示词管理、上下文记忆、工具调用、多步骤编排每一件单独拎出来都有一堆细节。Langchain和Langgraph就是冲着这些问题来的前者帮你把“接模型、写提示词、串流程”这些基础活标准化后者则更进一步用状态图的方式把复杂的多步骤工作流编排清楚。这篇文章我会围绕自己的实操经验展开先讲清楚这两个框架各自解决什么痛点再带大家用真实代码走一遍从简单链式调用到有状态工作流的完整路径。内容覆盖核心概念、代码示例、版本注意事项以及我在实际项目里踩过的坑。适合刚接触LLM应用开发、想系统了解Langchain和Langgraph或者已经在用但希望把工作流编排得更清晰的开发者。就算你现在一个Hello World都没写过跟着文章的思路走完一遍也能建立一套完整的认知框架。1. Langchain和Langgraph到底解决什么问题1.1 先把痛点摆出来LLM应用开发绕不开的几件事做个LLM应用表面上就是调用一下模型API把用户问题塞进提示词拿到返回结果。但只要接触过稍微真实一点的需求就会发现事情远没那么简单。第一类痛点是模型接入。市面上模型服务五花八门有闭源的也有开源的有按token计费的有自部署的每个服务商提供的SDK和消息格式还都不一样。今天用A家的模型明天想换B家的所有调用的地方都得跟着改一遍。这本质上跟当年数据库驱动的问题一样需要一层抽象把这些差异屏蔽掉。第二类痛点是流程编排。真实业务很少是“一个问题直接丢给模型”这么简单。做一个客服助手可能需要先判断用户意图再决定要不要查订单库查询完还要把结果组装成回答最后再判断需不需要转人工。每一步都有依赖关系有些步骤要循环执行有些步骤要按条件分支。单纯用代码if-else硬写短期内能跑但流程一复杂状态管理就失控了。第三类痛点是记忆和上下文。模型本身没有记忆每次调用都是无状态的。多轮对话、中间结果暂存、跨步骤传递信息这些都需要应用层自己维护。如果不做统一设计代码里到处塞着全局变量项目很快就乱成一锅粥。这三类痛点恰好就是Langchain和Langgraph主要的发力方向。1.2 Langchain的定位LLM应用的基础脚手架Langchain的核心贡献是把LLM应用开发中的通用能力抽象成了一套标准化组件。我用下来最主要的几块是模型封装层把不同提供方的模型统一成同一个接口。只要底层实现符合协议上层代码不用关心你用的是哪种模型切换成本变得很低。提示词模板层把提示词从散落在代码里的字符串提升为可管理、可复用的模板支持变量插值和格式化。输出解析层把模型返回的文本解析成结构化数据比如JSON、列表、对象。记忆管理则把对话历史封装起来给模型“加上记忆”。还有检索增强模块负责文档加载、切分、向量化、召回这一整套RAG链路。这些组件单独用价值有限组合起来才是Langchain的杀手锏。它提供了一种基于LCEL的描述式语法可以像流水线一样把多个组件串起来。每一段输出直接成为下一段的输入整个应用逻辑像管道一样清晰。如果你只做一个非常简单的、调用一两次模型的工具那确实没必要上Langchain直接写SDK调用还更轻。但一旦业务开始有多流程、多组件协作Langchain这套抽象的价值就体现出来了。它相当于LLM应用的脚手架把地基和管线都搭好你只需要往里面填充业务逻辑。1.3 Langgraph的定位从“链”升级到“有状态工作流”Langchain的链式调用解决的是线性流水线的问题但现实中的业务逻辑往往不是一条直线而是带分支、带循环、甚至需要动态规划路径的图结构。比如一个Agent应用模型在思考过程中可能需要反复调用多个工具根据工具返回结果再决定下一步动作。这种场景用固定链路很难优雅实现。Langgraph给出的解法是把工作流建模成一张状态图。一个典型的Langgraph应用由几个核心概念构成全局状态、执行节点、状态转移边以及条件分支。每个节点执行一段逻辑执行完成后可以更新全局状态边定义了节点之间的跳转关系条件边则根据当前状态判断下一步该往哪个节点走。和Langchain的线性管道相比Langgraph最大的优势在于它明确了状态的所有权和流转方式。每个节点能读哪些状态、能写哪些状态都在图定义里写清楚。既适合直观展示复杂工作流的整体结构也方便后续的调试和追查。这里要特别说清楚一个关系Langgraph不是要替代Langchain而是建立在Langchain组件基础之上的编排层。模型封装、提示词、检索这些基础能力仍然来自LangchainLanggraph负责的是这些能力之间的组织逻辑。我用过的最舒服的组合方式是Langchain提供零件Langgraph负责组装和调度。2. Langchain上手用核心组件搭起第一个应用骨架2.1 模型接入把不同模型服务统一成一个接口Langchain对模型接入的处理思路简单说就是“一切皆ChatModel”。不管是商用闭源模型、开源模型还是本地部署的模型服务Langchain把它们的消息输入输出格式统一成了同一套对话消息结构底层算子则封装在各个独立的包里面。以OpenAI协议为例实际使用中大多数模型服务都兼容这一协议所以只要一个统一的客户端类改一下接口地址和模型名就能完成切换from langchain_openai import ChatOpenAI llm ChatOpenAI( modelgpt-4o-mini, temperature0.2, timeout60, ) result llm.invoke(用一句话介绍你自己) print(result.content)依赖安装需要注意一点Langchain从某个版本开始按功能拆包模型接入、向量库、检索等被拆成了独立的依赖包。因此安装时要使用langchain-openai而不是在langchain主包里找模型接口。直接pip install langchain-openai即可。pip install langchain langchain-openai langgraph如果你用的是国内的一些模型服务通常只要设置base_url为对应服务地址并修改model名称即可。这套统一接口让我在项目里切换模型非常方便再也没有“B方案不可用就得改死代码”的尴尬。2.2 提示词模板把杂乱的提示词管理起来早期做提示词大家都是直接在代码里拼接字符串prompt 你是一个客服助手请回答用户的问题 user_question这样写一两个接口还行写到十个以上就会发现问题同一类提示词散落在各处改一句话要全局搜索替换而且很难复用。Langchain的提示词模板组件本质上是给提示词加了一层模板抽象支持定义固定的系统指令、可变的用户输入还可以直接加载模板文件。from langchain_core.prompts import ChatPromptTemplate prompt ChatPromptTemplate.from_messages([ (system, 你是一位严谨的技术文档审校员擅长发现描述中的歧义和逻辑漏洞。), (human, 请审校以下内容输出问题列表\n{text}), ]) filled prompt.format_messages(textLangchain是一个用于构建LLM应用的框架。)把提示词集中管理之后迭代成本大幅下降。我在实际项目里习惯把所有提示词模板放在单独的模块文件中按业务域分门别类既方便测试也方便做版本对比。另一个心得是给模板里的每处输入变量起清晰的名字并保持变量数量尽量少因为模板变量一多调用时漏传参数的几率就上来了。2.3 LCEL链式调用一行一行把能力串起来Langchain到目前为止最顺手的部分是LCEL。用“|”运算符把不同组件拼起来前一个组件的输出自动作为后一个组件的输入。它的设计思路和Unix管道一模一样每个组件都是“输入一些东西、输出另一些东西”只需保证接口兼容就能任意组合。简单场景下一条链由三部分组成from langchain_core.output_parsers import StrOutputParser from langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI llm ChatOpenAI(modelgpt-4o-mini, temperature0.2) prompt ChatPromptTemplate.from_messages([ (system, 你是一名信息抽取助手。), (human, 从以下文本中提取所有日期\n{text}), ]) chain prompt | llm | StrOutputParser() result chain.invoke({text: 项目将于2025年3月启动5月进行中期评审。}) print(result)这里prompt | llm会把一个字典格式的输入格式化成消息并交给模型llm | StrOutputParser则把模型返回的复杂消息对象转成纯文本字符串。LCEL真正有价值的地方在于它保留了运行时兼容性。一个简单的二元组链后续可以在任意位置插入新的环节而不用改调用方代码from langchain_core.output_parsers import PydanticOutputParser chain ( prompt | llm | PydanticOutputParser(pydantic_objectExtractedInfo) )开始用LCEL之前我建议先忍一忍不用传统Chain类直接走LCEL写法。它的调试信息、流式输出、异步支持都更完整已经是Langchain当前和后续版本的主推方向。越早切换到LCEL后面迁移成本越低。2.4 RAG检索增强让模型看到内部资料大模型的训练知识有截止时间也没法覆盖企业内部文档。RAG是目前最主流的解决方案思路并不复杂先把文档切成小块、做向量化存入向量库用户提问时先在库里检索出相关内容再把“用户问题检索片段”一起送入模型生成回答。Langchain在这一块同样提供了完整链路。文档加载、文本切分、向量化、向量存储、检索器封装都有对应的组件而且每一步都可以按需替换。from langchain_community.document_loaders import TextLoader from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import FAISS loader TextLoader(product_manual.txt) docs loader.load() splitter RecursiveCharacterTextSplitter(chunk_size500, chunk_overlap50) chunks splitter.split_documents(docs) vectorstore FAISS.from_documents(chunks, OpenAIEmbeddings()) retriever vectorstore.as_retriever(search_kwargs{k: 3})实际使用中切分参数对检索效果影响很大。块太小语义不够完整块太大噪声多且检索不精确。我之前在一个产品手册问答项目里做过对比chunk_size500、chunk_overlap50是综合效果比较稳的起点具体还是应该根据文档类型调整。技术手册可以小一点政策文件需要大块保持上下文完整。Langchain把RAG链路拆成组件不是单纯为了炫技而是为了让你能够单独替换链路中的任何一环。比如今天用向量库做召回明天想换成全文检索只需要改一行代码测试维护成本很低。3. Langgraph实战从链式调用到可控的状态图编排3.1 为什么链路越复杂越需要状态图用LCEL做线性流程非常舒服但一旦涉及循环和分支链式的短板就显出来了。举个实际场景做一个内容分析助手要对一篇文章做主题识别、情感分析和摘要生成。如果只是顺序跑三步LCEL勉强可以串起来。可如果业务要求“文章长度超过一定阈值先做段落拆分再逐段分析”或者“情感分析结果异常时需要额外的复核节点”线性链就开始别扭了。这些需求本质上是“状态依赖”的需求。下一步做什么取决于这一步的结果。Langgraph把这类流程建模成图让每一个执行节点都能读取和更新全局状态再通过条件边决定状态流转的方向。好处是流程结构一目了然在图上就能看到所有路径而不是在层层嵌套的if-else里艰难推理。3.2 核心概念State、Node、Edge和条件边Langgraph的四个核心概念理解透了就掌握了一个基本模型。State是贯穿整个工作流的全局状态通常是一个TypedDict里面定义了你需要在节点之间传递的所有数据。每个节点执行完后可以返回更新内容Langgraph会把更新合并回全局状态。Node是一段业务逻辑函数接收当前状态作为入参返回一份更新后的状态字典。Edge是连接两个节点的箭头表示一个节点执行完应该去哪个节点。条件边是在边上附加一个路由函数根据当前状态返回下一步节点名从而实现分支和循环。from typing import TypedDict from langgraph.graph import StateGraph, START, END class ContentState(TypedDict): text: str topic: str sentiment: str summary: str和普通函数式编程的区别在于节点之间不直接传参而是通过共享状态互相通信。这种设计在工程上的好处是可以随时插入一个新节点不需要改动其他节点的代码只要它读写状态的地方定义清楚就行。3.3 手写一个多步骤内容分析流程我以一个实际写过的内容分析助手为例演示Langgraph的基本用法。这个应用接收一段产品评论文本按顺序做主题分类、情感评分、观点摘要并且加入一个判断节点如果文本长度超过限定值先做拆分预处理再进入正式分析流程。先定义状态与节点函数from typing import TypedDict from langgraph.graph import StateGraph, START, END from langchain_openai import ChatOpenAI class AnalysisState(TypedDict): text: str preprocessed: str topic: str sentiment: str summary: str def preprocess(state: AnalysisState): text state[text] if len(text) 200: text text[:200] …已截断 return {preprocessed: text} def classify_topic(state: AnalysisState): llm ChatOpenAI(modelgpt-4o-mini, temperature0.1) prompt f判断下面这段评论的主题类别只输出一个词。可选性能/外观/价格/售后。\n{state[preprocessed]} topic llm.invoke(prompt).content.strip() return {topic: topic} def analyze_sentiment(state: AnalysisState): llm ChatOpenAI(modelgpt-4o-mini, temperature0.1) prompt f对下面评论做情感分析输出一个整数分数范围1-5。\n{state[preprocessed]} sentiment llm.invoke(prompt).content.strip() return {sentiment: sentiment} def summarize(state: AnalysisState): llm ChatOpenAI(modelgpt-4o-mini, temperature0.3) prompt f用一句话概括这条评论的核心观点。\n{state[preprocessed]} summary llm.invoke(prompt).content.strip() return {summary: summary}然后构建图builder StateGraph(AnalysisState) builder.add_node(preprocess, preprocess) builder.add_node(classify, classify_topic) builder.add_node(sentiment, analyze_sentiment) builder.add_node(summary, summarize) builder.add_edge(START, preprocess) builder.add_edge(preprocess, classify) builder.add_edge(classify, sentiment) builder.add_edge(sentiment, summary) builder.add_edge(summary, END) graph builder.compile()调用方式和Langchain链对象一致result graph.invoke({text: 这款耳机音质出色低频扎实但佩戴久了有点夹头总体对得起这个价位。}) print(result[topic]) print(result[sentiment]) print(result[summary])如果你想在这种顺序流程上再加分支比如当情感分数低于2分时额外走一个“复评确认”节点只需在情感分析后面加一条条件边def route_by_sentiment(state: AnalysisState): try: score int(state[sentiment]) except ValueError: score 3 if score 2: return recheck return summary builder.add_node(recheck, recheck) builder.add_edge(sentiment, recheck) builder.add_conditional_edges(sentiment, route_by_sentiment, { recheck: recheck, summary: summary, })这段逻辑如果用传统LCEL链加上if-else来写流程一复杂就完全看不出整体结构了。而在Langgraph里所有路径都显式地画在图上业务方评审时可以直接对着图讨论这种可视化价值在真实项目里非常重要。3.4 更省力的Agent封装create_react_agent如果你只是想快速搭一个能让模型自主调用工具的AgentLanggraph还提供了更高层的封装不必每次从零手写状态图和路由逻辑。比如create_react_agent就内置了“模型-工具-循环”的标准实现模型根据用户问题决定调用哪个工具拿到工具结果后继续推理直到不需要调用工具为止。from langchain_openai import ChatOpenAI from langgraph.prebuilt import create_react_agent def search_weather(city: str) - str: 查询指定城市的天气情况。 # 实际项目里可以对接天气API return f{city}今天多云气温8到15摄氏度。 tools [search_weather] llm ChatOpenAI(modelgpt-4o-mini, temperature0.2) agent create_react_agent(llm, tools) response agent.invoke({messages: [(human, 上海今天适合穿什么衣服)]}) print(response[messages][-1].content)create_react_agent封装得越好隐藏的细节越多。真正要用好它建议还是先准确理解底层状态图和工具调用的机制不然遇到“Agent陷入死循环”或者”工具参数传得不对“这类问题时会无从下手。我的建议是初学阶段用0.2版本的手写图练手理解之后再切换到高层封装提高速度。4. 版本、报错与调试我踩过的几个坑4.1 依赖版本不一致是最容易翻车的点Langchain和Langgraph的版本迭代速度非常快API调整也比较频繁。早先有些包里的类名到了新版本可能就被移除或改了位置。比如模型输出解析相关的导入路径就调整过多次。解决方案有两个一是锁定依赖版本号在requirements.txt里把版本固定住不要总是装最新版二是留意版本兼容表。用Langgraph时配合的Langchain版本需要选中一个稳定组合不然会出现langgraph依赖的接口在新版langchain里已经移除这类问题跑起来全是报错。我目前的建议是新项目直接用较新的稳定版本老项目不要频繁升级核心框架版本。把框架当作基础设施稳定性优先。4.2 状态更新与共享的坑Langgraph的State更新机制不同版本差别比较大。旧版关于状态归并的写法在新版中已经不推荐而是推荐在字段上声明归并函数。不声明时字段默认是直接覆盖如果希望节点返回内容追加到已有状态比如对话消息列表就需要自己实现一个追加逻辑。另一个容易踩的坑是在节点里修改了传入的state字典但忘了返回值。节点函数返回的字典才是真正用于更新状态的直接修改外部状态是不生效的。我刚开始用的时候调试了很久才发现节点里某条逻辑更新了字典却没在返回语句里带出来。4.3 调试Langgraph的几种有效姿势调试Langgraph首先要善用Langsmith或在线调试面板。只要设置好环境变量每次运行都会记录执行轨迹可以看到每一步调用模型、工具的输入输出精确定位问题节点。如果不方便用在线面板也可以在节点函数里加临时日志。Langgraph节点是普通函数可以在入口输出当前state的关键信息在返回前输出更新内容。我在实际项目里会把默认调试输出关掉只在某个节点里临时加print跑完再删掉。保持代码干净也方便复现问题。有一点要提醒状态里如果有长文本print调试时会被刷屏。我的做法是只打印关键字段名和长度或者截取前100个字符。4.4 常见问题速查表我把开发过程中遇到过的高频问题整理成一张表方便大家对照排查。现象可能原因解决办法导入类时报模块不存在版本太老或包拆分导致路径变化检查完整依赖名称按对应版本调整导入路径节点更新状态后其他地方读不到节点没有返回更新字典确认节点返回值包含所有需要更新的字段Agent反复调用同一工具不停提示词不明确或工具返回结果没有触及停条件优化工具描述检查循环上限设置给工具返回增加结构化结果标识链式调用时变量未传入Prompt模板里用了变量但invoke没有传完对比模板变量和传入字典的键名确保完全一致回调输出大量重复日志核心链路被重复执行通常是条件分支配置有问题在图上增加调试输出检查每个条件边路由结果是否符合预期同一模型在LCEL下输出结构变化温度参数或模型设定在不同调用间不一致统一模型配置把模型实例抽为公共组件复用排查思路归纳起来就一句话先看有没有报错再定位是状态问题还是路径问题最后用轨迹工具确认。结尾我在实际项目中体会最深的一点是不要被框架概念吓住。Langchain和Langgraph本质上就是在回答一个问题如何让大模型应用从“写脚本”变成“做系统”。前者的组件抽象帮你把基础设施理顺后者的状态图帮你在复杂流程中保持清晰。把这两个框架用熟收益会随着项目复杂度提升越来越明显。最后分享一个小技巧刚开始接触Langgraph时不要一上来就追求用高级封装而是从最简单的三节点图写起先熟悉State和边的运转方式。等你能不假思索地说出“当前状态更新到了哪里、下一步该走哪条边”再迁移到高层封装或者自研编排都会从容得多。这种先底层后封装的路径我每次带新人走一遍效果都好过直接甩一堆文档。
阅读完成 · 觉得有帮助?
咨询建站