LangChain 与 LangGraph 实战:构建可编排的大模型智能应用
一、为什么需要 LangChain 和 LangGraph
直接调用大模型 API 很容易,但当应用变得复杂时,开发者很快会遇到以下问题:
- Prompt 散落在各个业务函数中;
- 多轮对话上下文难以管理;
- 工具调用逻辑与模型调用逻辑混杂;
- RAG 检索流程难以复用;
- 任务失败后无法恢复;
- 多个 Agent 之间缺乏清晰的协作关系;
- 复杂流程只能通过大量
if else维护。
LangChain 主要解决的是“大模型应用组件化”问题。它提供了模型封装、Prompt 模板、输出解析器、工具调用、检索器和 Agent 等组件。
LangGraph 则进一步解决“复杂流程编排”问题。它把大模型应用表示为一个有状态的图:
输入 | v 意图识别 | +--> 知识库检索 | +--> 工具调用 | v 答案生成 | v 输出LangChain 更像是组件库,LangGraph 更像是工作流引擎。两者可以结合使用。
二、安装依赖
本文使用 Ollama 作为本地模型服务,也可以替换为 OpenAI、通义千问或其他兼容模型。
安装依赖:
pipinstall-Ulangchain langchain-ollama langgraph pydantic如果需要向量检索,可以额外安装:
pipinstallchromadb langchain-chroma下载 Ollama 模型:
ollama pull qwen2.5:7b三、LangChain 调用本地模型
最简单的模型调用代码如下:
fromlangchain_ollamaimportChatOllama llm=ChatOllama(model="qwen2.5:7b",temperature=0.2)response=llm.invoke("请解释什么是LangChain")print(response.content)ChatOllama返回的是一个消息对象,而不是普通字符串。生成的文本通常位于:
response.content3.1 使用消息角色
fromlangchain_core.messagesimportSystemMessage,HumanMessage messages=[SystemMessage(content="你是一名资深软件架构师。"),HumanMessage(content="如何设计一个高可用的消息队列系统?")]response=llm.invoke(messages)print(response.content)消息角色包括:
SystemMessage:定义模型角色和行为;HumanMessage:用户输入;AIMessage:模型输出;ToolMessage:工具执行结果。
通过消息类型组织上下文,比手动拼接字符串更加清晰。
四、使用 Prompt 模板
将 Prompt 直接写在业务代码里,会导致后续维护困难。LangChain 提供了模板机制。
fromlangchain_core.promptsimportChatPromptTemplate prompt=ChatPromptTemplate.from_messages([("system","你是一名专业技术作者,请使用清晰、准确的语言回答问题。"),("human","请围绕以下主题写一段技术说明:{topic}")])chain=prompt|llm result=chain.invoke({"topic":"向量数据库的索引原理"})print(result.content)这里的|表示 Runnable 组合。执行过程如下:
输入变量 | Prompt模板 | ChatOllama | AIMessage这比手动执行多个函数更加符合声明式编程思想。
五、结构化输出
大模型输出自然语言,程序很难直接解析。实际业务中通常需要模型返回固定结构。
定义 Pydantic 模型:
frompydanticimportBaseModel,FieldfromtypingimportLiteralclassIntentResult(BaseModel):intent:Literal["knowledge","weather","general"]confidence:float=Field(ge=0,le=1,description="意图识别置信度")reason:str让模型输出结构化结果:
structured_llm=llm.with_structured_output(IntentResult)result=structured_llm.invoke("用户问:今天北京天气怎么样?")print(result)print(result.intent)print(result.confidence)结构化输出的意义在于把模型输出转换为程序可使用的数据对象,后续可以直接根据intent选择不同处理流程。
需要注意,结构化输出并不意味着模型绝对可靠。生产环境仍然需要:
- 对字段进行校验;
- 处理解析失败;
- 设置默认值;
- 限制枚举范围;
- 记录异常响应。
六、实现一个简单的 RAG Chain
6.1 创建向量库
fromlangchain_ollamaimportOllamaEmbeddingsfromlangchain_chromaimportChroma embeddings=OllamaEmbeddings(model="nomic-embed-text")documents=["系统发布需要经过开发、测试、预发布和生产审批。","员工报销需要在费用发生后30天内提交。","数据库变更必须先执行备份,再执行变更脚本。"]vectorstore=Chroma.from_texts(texts=documents,embedding=embeddings,collection_name="company_knowledge",persist_directory="./chroma_data")retriever=vectorstore.as_retriever(search_kwargs={"k":3})6.2 构建检索问答链
fromlangchain_core.promptsimportChatPromptTemplate rag_prompt=ChatPromptTemplate.from_template(""" 你是企业知识库助手。 请根据上下文回答问题。 如果上下文中没有答案,请明确说明无法从知识库中找到依据。 不要根据常识补充未提供的信息。 上下文: {context} 问题: {question} """)defformat_documents(docs):return"\n\n".join(doc.page_contentfordocindocs)defrag_answer(question:str):docs=retriever.invoke(question)context=format_documents(docs)chain=rag_prompt|llm response=chain.invoke({"context":context,"question":question})return{"answer":response.content,"sources":[doc.page_contentfordocindocs]}result=rag_answer("数据库变更前需要做什么?")print(result["answer"])一个可靠的 RAG 系统不应该只返回答案,还应返回来源信息。这样用户可以判断答案是否有依据。
七、为什么复杂任务需要 LangGraph
假设我们要构建一个智能助手,它需要完成以下流程:
- 判断用户意图;
- 如果是知识库问题,执行知识检索;
- 如果是天气问题,调用天气工具;
- 如果是普通问题,直接让模型回答;
- 检查答案是否存在依据;
- 必要时重新检索;
- 最后输出结果。
如果使用普通函数,代码可能变成大量嵌套判断:
ifintent=="knowledge":docs=retrieve()ifnotdocs:...elifintent=="weather":...else:...LangGraph 将这些步骤拆分成节点,并通过边描述节点之间的跳转关系。这样流程更加清晰,也方便增加重试、人工审批和持久化。
八、定义 LangGraph 状态
状态是图中所有节点共享的数据结构。
fromtypingimportTypedDict,Listfromlangchain_core.messagesimportBaseMessageclassAgentState(TypedDict,total=False):question:strintent:strconfidence:floatdocuments:List[str]answer:strgrounded:boolmessages:List[BaseMessage]total=False表示字段可以不是一开始就全部存在。不同节点可以逐步补充状态。
九、实现意图识别节点
frompydanticimportBaseModelfromtypingimportLiteralclassIntent(BaseModel):name:Literal["knowledge","weather","general"]confidence:floatintent_llm=llm.with_structured_output(Intent)defclassify_intent(state:AgentState):result=intent_llm.invoke(f"请判断下面问题属于哪一类:知识库、天气或普通问题。\n"f"问题:{state['question']}")return{"intent":result.name,"confidence":result.confidence}节点函数接收状态,返回需要更新的字段。它不需要手动传递所有变量。
十、实现知识检索节点
defretrieve_documents(state:AgentState):docs=retriever.invoke(state["question"])return{"documents":[doc.page_contentfordocindocs]}为了避免将过多信息传给模型,可以限制内容大小:
defretrieve_documents(state:AgentState):docs=retriever.invoke(state["question"])contents=[]total_length=0fordocindocs:text=doc.page_contentiftotal_length+len(text)>5000:breakcontents.append(text)total_length+=len(text)return{"documents":contents}这是一个很重要的工程细节。检索结果越多不一定越好。过长的上下文可能导致:
- 推理速度变慢;
- 关键内容被淹没;
- Token 成本增加;
- 模型回答出现偏离。
十一、实现答案生成节点
answer_prompt=ChatPromptTemplate.from_template(""" 你是一个严谨的企业智能助手。 请根据提供的上下文回答问题。 上下文: {context} 用户问题: {question} 要求: 1. 不要编造上下文之外的事实; 2. 如果资料不足,明确说明; 3. 使用简洁、专业的中文回答。 """)defgenerate_answer(state:AgentState):context="\n\n".join(state.get("documents",[]))response=(answer_prompt|llm).invoke({"context":context,"question":state["question"]})return{"answer":response.content}十二、实现答案校验节点
大模型生成答案后,可以让另一个模型判断答案是否有上下文支持。
classGroundingResult(BaseModel):grounded:boolreason:strgrounding_llm=llm.with_structured_output(GroundingResult)defcheck_grounding(state:AgentState):context="\n\n".join(state.get("documents",[]))prompt=f""" 请判断下面的答案是否能够从上下文中得到支持。 上下文:{context}问题:{state["question"]}答案:{state["answer"]}请判断答案是否有充分依据。 """result=grounding_llm.invoke(prompt)return{"grounded":result.grounded}这种“生成 + 校验”的方式可以降低明显幻觉,但它并不能从根本上保证事实正确。校验模型本身也可能判断错误,因此关键业务仍然需要人工审核或规则系统兜底。
十三、组装 LangGraph
fromlanggraph.graphimportStateGraph,START,END builder=StateGraph(AgentState)builder.add_node("classify",classify_intent)builder.add_node("retrieve",retrieve_documents)builder.add_node("generate",generate_answer)builder.add_node("check",check_grounding)定义意图路由:
defroute_intent(state:AgentState):intent=state.get("intent")ifintent=="knowledge":return"retrieve"ifintent=="weather":return"weather"return"generate_general"为了简化示例,天气和普通问题节点如下:
defweather_node(state:AgentState):return{"answer":"天气工具尚未配置,请先接入实际天气API。"}defgeneral_node(state:AgentState):response=llm.invoke(state["question"])return{"answer":response.content}builder.add_node("weather",weather_node)builder.add_node("generate_general",general_node)添加边:
builder.add_edge(START,"classify")builder.add_conditional_edges("classify",route_intent,{"retrieve":"retrieve","weather":"weather","generate_general":"generate_general"})builder.add_edge("retrieve","generate")builder.add_edge("generate","check")builder.add_edge("weather",END)builder.add_edge("generate_general",END)定义校验后的路由:
defroute_grounding(state:AgentState):ifstate.get("grounded"):return"finish"return"retry"增加重试节点:
defretry_retrieve(state:AgentState):docs=retriever.invoke(state["question"]+" 请检索更具体、更相关的资料")return{"documents":[doc.page_contentfordocindocs]}builder.add_node("retry",retry_retrieve)builder.add_conditional_edges("check",route_grounding,{"finish":END,"retry":"retry"})builder.add_edge("retry","generate")编译图:
graph=builder.compile()执行:
result=graph.invoke({"question":"数据库变更前需要执行什么操作?"})print(result["answer"])print("是否有依据:",result.get("grounded"))完整流程如下:
START | classify | +--> weather ------> END | +--> generate_general -> END | retrieve | generate | check | +--> grounded ------> END | retry | generate十四、使用流式输出观察执行过程
LangGraph 支持流式获取节点执行结果:
foreventingraph.stream({"question":"系统发布需要经过哪些流程?"},stream_mode="updates"):print(event)输出可能类似:
{'classify':{'intent':'knowledge','confidence':0.96}}{'retrieve':{'documents':['系统发布需要经过开发、测试、预发布和生产审批。']}}{'generate':{'answer':'系统发布需要经过开发、测试、预发布和生产审批。'}}{'check':{'grounded':True}}流式事件对于调试非常重要。传统的黑盒调用只能看到最终答案,而图流式输出可以观察每个节点的状态变化。
十五、接入工具调用
智能 Agent 通常需要调用外部工具。下面定义一个简单的天气工具:
fromlangchain_core.toolsimporttool@tooldefget_weather(city:str)->str:"""查询指定城市的天气。"""weather_data={"北京":"晴,25摄氏度","上海":"多云,28摄氏度","广州":"小雨,30摄氏度"}returnweather_data.get(city,"暂时没有该城市的天气数据")模型绑定工具:
tool_llm=llm.bind_tools([get_weather])response=tool_llm.invoke("请查询北京的天气")print(response.tool_calls)需要注意,模型返回工具调用请求并不代表工具已经执行。应用程序需要解析tool_calls,执行对应函数,再把结果返回给模型。
一个简单的执行逻辑:
defexecute_tools(message):outputs=[]forcallinmessage.tool_calls:ifcall["name"]=="get_weather":result=get_weather.invoke(call["args"])outputs.append(result)returnoutputs在实际项目中,应该建立工具注册表,而不是使用大量if else:
TOOLS={"get_weather":get_weather}defexecute_tools(message):results=[]forcallinmessage.tool_calls:tool=TOOLS.get(call["name"])iftoolisNone:raiseValueError(f"未知工具:{call['name']}")result=tool.invoke(call["args"])results.append(result)returnresults十六、LangChain 与 LangGraph 的职责划分
可以使用下面的方式理解二者关系:
| 组件 | 主要职责 |
|---|---|
| Chat Model | 调用大语言模型 |
| Prompt Template | 管理输入模板 |
| Output Parser | 解析模型输出 |
| Retriever | 检索外部知识 |
| Tool | 封装外部能力 |
| Chain | 串联固定处理步骤 |
| LangGraph | 编排条件分支和循环流程 |
| Checkpointer | 保存流程状态 |
| Human-in-the-loop | 引入人工审批 |
如果流程是固定的,例如:
输入 -> Prompt -> 模型 -> 解析使用 LangChain Chain 就足够。
如果流程包含条件、循环、重试、人工审批和多个 Agent,则应当使用 LangGraph。
十七、生产环境设计建议
17.1 节点必须尽量保持单一职责
一个节点最好只完成一件事。例如:
- 意图识别节点只负责分类;
- 检索节点只负责检索;
- 生成节点只负责生成;
- 校验节点只负责判断。
这样更容易测试,也容易替换。
17.2 所有外部调用都要设置超时
工具调用、向量数据库调用和模型调用都可能失败。不能让一个节点无限等待。
17.3 对状态进行裁剪
状态会随着流程不断增长。如果把完整对话、所有检索文档和所有工具结果都放进去,长流程可能导致上下文过大。
17.4 使用持久化保存状态
复杂任务可能运行很长时间,或者需要人工确认。此时应该使用 Checkpointer 保存状态,以便中断后继续。
17.5 不要让 Agent 拥有无限权限
工具应该遵循最小权限原则。例如:
- 只读数据库工具与写入工具分开;
- 高风险操作必须人工审批;
- 限制工具参数范围;
- 记录每次工具调用;
- 对敏感数据进行脱敏。
十八、测试一个 LangGraph 应用
不要只测试最终答案,还应该测试每个节点。
deftest_intent():result=classify_intent({"question":"数据库变更前需要做什么?"})assertresult["intent"]in{"knowledge","weather","general"}测试检索节点:
deftest_retrieval():result=retrieve_documents({"question":"系统发布流程是什么?"})assert"documents"inresultassertlen(result["documents"])>0测试图的完整执行:
deftest_graph():result=graph.invoke({"question":"数据库变更前需要执行什么操作?"})assertresult.get("answer")对于大模型应用,测试重点不只是字符串是否完全相等,还包括:
- 是否返回结构化数据;
- 是否调用正确工具;
- 是否包含来源;
- 是否在资料不足时拒答;
- 是否能够从失败节点恢复;
- 是否不会泄露敏感信息。
十九、总结
LangChain 解决的是大模型应用开发中的组件复用问题,LangGraph 解决的是复杂任务中的流程编排问题。
一个比较完整的企业级智能应用通常可以拆分为:
用户输入 | 意图识别 | 流程路由 | 检索 / 工具 / 普通问答 | 结果生成 | 事实校验 | 人工审批或最终输出LangChain 适合封装模型、Prompt、工具和检索器;LangGraph 适合表达条件分支、循环、重试和状态管理。
在实践中,不建议一开始就把所有逻辑都设计成复杂 Agent。更合理的路径是:
- 先使用 LangChain 实现最小可用链路;
- 再把固定流程拆成独立节点;
- 当出现条件分支和循环时引入 LangGraph;
- 最后补充状态持久化、监控、人工审批和安全控制。
真正可靠的大模型应用,不是简单地把模型接入系统,而是围绕模型建立一套可观测、可恢复、可验证的工程流程。