1. 这不是“LangChain教程”,而是一份能让你少踩三个月坑的实战手记
“LangChain教程 仅供参考”——看到这个标题,我第一反应是笑。不是笑标题敷衍,而是笑自己去年刚入局时,也对着满屏“LangChain入门”“LangChain保姆级教程”反复点开、复制、报错、删库、重来。那些教程里写着“pip install langchain”,却没告诉你装完后 import langchain 报错 ModuleNotFoundError;写着“调用 OpenAI API”,却没提国内网络环境下 base_url 怎么填、API Key 从哪来、环境变量为什么死活不生效;更没人告诉你,当你兴冲冲跑通第一个 ChatModel 后,紧接着在 Agent 链里卡住三天,只因一个 tools 参数传了 list 却没加 tool_choice,或者 memory 的 stateful 实现根本没初始化……这些不是细节,是门槛。而今天这篇,就是我把过去一年在真实业务中——从给本地知识库搭 RAG 检索服务,到用百炼 Qwen-Plus 构建客服对话引擎,再到把 LangGraph 流程嵌进 Spring Boot 微服务——所有掉过的坑、试过的解法、压测过的效果,全盘托出。它不叫“教程”,因为教程教你怎么走,而这份手记告诉你:哪条路有碎玻璃,哪段坡必须换挡,哪个路口的红灯永远比绿灯长三秒。关键词就三个:LangChain、Agents、阿里云百炼——所有内容只围绕这三者的真实交集展开,不讲虚的架构图,不堆概念金字塔,只给你能直接粘贴、修改、上线的代码块,和每行代码背后“为什么非得这么写”的硬核理由。
2. LangChain 的本质不是框架,而是“大模型能力的标准化插线板”
很多人一上来就问:“LangChain 是干嘛的?”官方回答是“LLM 应用开发框架”。但这句话对新手毫无意义。我更愿意把它比作一个标准化插线板——你家里的电器(大模型)品牌各异(OpenAI、百炼、Ollama、本地 Llama),电压不同(API 格式:OpenAI 兼容模式 / DashScope 原生模式 / 自定义 REST),接口形状也不一样(JSON Schema、流式响应 chunk 结构、错误码定义)。LangChain 就是那个统一规格的插线板:它不管你插进来的是美的空调还是格力冰箱,只要符合它的“插座标准”,就能通电、开关、调档。这个“标准”,就是 LangChain 定义的Runnable 接口。
提示:Runnable 是 LangChain v0.1+ 的核心抽象,所有组件(LLM、Tool、Retriever、Chain)都必须实现 invoke()、stream()、batch() 等方法。这不是设计癖,而是为了解耦——你的业务逻辑不用关心底层模型是 HTTP 调用还是本地加载,只要调用 run.invoke(input) 就行。
所以,当你看到ChatOpenAI或ChatTongyi,别被名字迷惑。它们不是“OpenAI 模型封装”或“通义千问封装”,而是实现了 Runnable 接口的、适配特定模型 API 规范的客户端。关键区别在于:
ChatOpenAI:严格遵循 OpenAI 官方 API 规范(/v1/chat/completions),要求base_url指向兼容该规范的服务端点;ChatTongyi:适配 DashScope 原生 API(/api/v1/services/aigc/text-generation/generation),参数名、请求体结构、错误返回都按阿里云文档走。
这就解释了为什么你在百炼控制台看到“OpenAI 兼容模式”和“DashScope 原生模式”两个开关——前者是让百炼假装成 OpenAI,后者是让它做自己。选哪个?看你的 LangChain 组件。如果你用langchain_openai,就必须开兼容模式;如果用langchain_community.chat_models.tongyi,就必须关兼容模式,走原生路径。
实操中,我见过最多的问题是混用。比如:装了langchain_openai,却把base_url设成https://dashscope.aliyuncs.com/api/v1/...(原生地址),结果报 404;或者装了langchain-community,却用ChatOpenAI类去初始化,结果model="qwen-plus"不被识别。根源全在这里:插线板型号(包)和插座规格(API 模式)必须严格匹配。
验证方法极简单:打开终端,执行pip list | grep langchain,确认安装的是langchain-openai还是langchain-community;再查代码里 import 的类名和初始化参数,对照阿里云文档的“OpenAI 兼容模式支持的模型列表”和“DashScope 原生模型列表”。两者不一致,99% 的报错都源于此。别急着改代码,先校准这个基础认知——这是所有后续操作的地基。
3. 百炼模型接入:从环境变量到流式响应,绕不开的五个生死关
把百炼模型接入 LangChain,看似就几行代码,实则暗藏五道必须跨过的“生死关”。跨不过,轻则报错退出,重则服务假死、内存溢出。我用 Qwen-Plus 在生产环境压测时,就因第三关没过,导致 50 并发下响应延迟飙升至 12 秒。下面逐个拆解,附真实报错日志和修复方案。
3.1 第一关:API Key 的“隐形污染”与环境变量加载时机
最隐蔽的坑。你以为os.getenv("DASHSCOPE_API_KEY")拿到的是干净字符串?错。很多教程让你把 Key 写进.env文件,然后pip install python-dotenv,再load_dotenv()。问题来了:.env文件里如果多了一个空格、一个换行、甚至中文全角符号,os.getenv()返回的 Key 就带上了不可见字符。百炼服务端校验时直接返回{"code":"InvalidParameter","message":"Invalid API key"},你翻遍文档都找不到原因。
实测解决方案:
import os from dotenv import load_dotenv # 必须显式指定编码,且 strip() 去除首尾空白 load_dotenv(encoding='utf-8') raw_key = os.getenv("DASHSCOPE_API_KEY", "") clean_key = raw_key.strip() # 关键!必须 strip # 验证是否有效(不调用模型,只检查格式) if not clean_key or len(clean_key) < 32: raise ValueError(f"API Key 无效: '{raw_key}' (length: {len(raw_key)})") print(f"✅ API Key 加载成功,长度: {len(clean_key)}")注意:
encoding='utf-8'是必须的。Windows 记事本保存的.env默认是 GBK,不指定编码会读成乱码,Key 变成一堆b'\xe4\xb8\xad\xe6\x96\x87'字节,必然失败。
3.2 第二关:base_url 的“协议陷阱”与路径尾斜杠
base_url看似简单,但百炼文档里写了两套地址:
- OpenAI 兼容模式:
https://dashscope.aliyuncs.com/compatible-mode/v1 - DashScope 原生模式:
https://dashscope.aliyuncs.com/api/v1
很多人复制粘贴时,手一抖,把/v1写成/v1/(末尾多了斜杠)。后果?ChatOpenAI初始化时不会报错,但第一次invoke()时,Requests 库会把https://xxx/v1//chat/completions当作最终 URL,多出的//导致 400 Bad Request。错误日志里只显示HTTPError: 400 Client Error,根本看不出是斜杠惹的祸。
安全写法(强制校验):
from urllib.parse import urljoin, urlparse def validate_base_url(base_url: str) -> str: """校验并标准化 base_url,移除末尾斜杠""" if not base_url.startswith(("http://", "https://")): raise ValueError(f"base_url 必须以 http:// 或 https:// 开头: {base_url}") # 移除末尾斜杠,urljoin 会自动处理 parsed = urlparse(base_url) clean_path = parsed.path.rstrip('/') return f"{parsed.scheme}://{parsed.netloc}{clean_path}" # 使用 base_url = "https://dashscope.aliyuncs.com/compatible-mode/v1/" validated_url = validate_base_url(base_url) # 输出: https://dashscope.aliyuncs.com/compatible-mode/v1 print(f"✅ base_url 校验通过: {validated_url}")3.3 第三关:流式响应(Streaming)的“内存雪崩”与 Chunk 处理
streaming=True很酷,但它是双刃剑。ChatTongyi的stream()方法返回一个生成器,如果你直接for chunk in llm.stream(messages): print(chunk.content),看似没问题。但在高并发场景下,每个请求都会维持一个长连接,生成器对象长期驻留内存。我们曾在线上环境发现:100 个并发流式请求,Python 进程 RSS 内存占用从 200MB 暴涨到 2.3GB,GC 都来不及回收。
根本原因:ChatTongyi的stream()默认使用QwenStreamingChatModel,其内部缓冲区未做限流,大量小 chunk 持续涌入,Python 的list.append()在频繁扩容时产生内存碎片。
生产级修复(分块聚合 + 超时熔断):
from langchain_community.chat_models.tongyi import ChatTongyi from langchain_core.messages import HumanMessage, SystemMessage import time from typing import Generator, Dict, Any class SafeStreamingChatTongyi(ChatTongyi): def stream( self, input: Any, config: Optional[Dict] = None, **kwargs: Any, ) -> Generator[str, None, None]: # 设置超时,避免单个流挂起整个进程 start_time = time.time() timeout = kwargs.pop("timeout", 30.0) # 默认 30 秒 # 聚合最小输出单元:至少 10 个 token 或 0.5 秒才 yield 一次 buffer = "" token_count = 0 last_yield_time = time.time() for chunk in super().stream(input, config, **kwargs): if time.time() - start_time > timeout: raise TimeoutError(f"Stream timeout after {timeout}s") content = getattr(chunk, "content", "") if not content: continue buffer += content token_count += len(content.encode('utf-8')) // 4 # 粗略估算 token 数 # 满足任一条件则 yield if (token_count >= 10 or time.time() - last_yield_time >= 0.5 or "。" in content or "!" in content or "?" in content): yield buffer buffer = "" token_count = 0 last_yield_time = time.time() # 使用 llm = SafeStreamingChatTongyi( model="qwen-plus", dashscope_api_key=clean_key, streaming=True, temperature=0.3 ) messages = [SystemMessage(content="你是一个严谨的技术文档助手"), HumanMessage(content="请用三句话解释 LangChain 的核心价值")] for text in llm.stream(messages): print(f"▶ {text}")3.4 第四关:Tools 调用的“参数幻觉”与 JSON Schema 陷阱
Agent 最让人抓狂的,是工具调用时tool_input字段莫名消失。比如你定义了一个SearchTool,参数是{"query": "LangChain 教程"},但百炼返回的tool_calls里却是{"name": "search", "arguments": "{\"query\": \"LangChain 教程\"}"}—— 注意,arguments是字符串,不是 dict!LangChain 的Tool类默认期望arguments是 dict,于是解析时报JSONDecodeError。
根因:百炼的tool_calls返回格式是 OpenAI 兼容模式下的标准 JSON 字符串,但langchain-community的ChatTongyi在解析时,没有像langchain-openai那样自动json.loads(arguments)。
补丁方案(重写 _parse_tool_calls):
from langchain_community.chat_models.tongyi import ChatTongyi from langchain_core.messages import AIMessage import json class FixedChatTongyi(ChatTongyi): def _parse_tool_calls(self, message: AIMessage) -> List[Dict]: """修复 tool_calls arguments 字符串未解析问题""" if not hasattr(message, "additional_kwargs") or "tool_calls" not in message.additional_kwargs: return [] tool_calls = message.additional_kwargs["tool_calls"] fixed_calls = [] for call in tool_calls: try: # 如果 arguments 是字符串,尝试解析 if isinstance(call.get("function", {}).get("arguments"), str): args_str = call["function"]["arguments"] # 移除可能的 Markdown 代码块包裹 args_str = args_str.strip().strip('`').strip() if args_str.startswith('{') and args_str.endswith('}'): call["function"]["arguments"] = json.loads(args_str) except (json.JSONDecodeError, Exception) as e: print(f"⚠️ 工具参数解析失败: {e}, 原始 arguments: {call.get('function', {}).get('arguments')}") continue fixed_calls.append(call) return fixed_calls # 使用 FixedChatTongyi 替代 ChatTongyi llm = FixedChatTongyi(model="qwen-plus", dashscope_api_key=clean_key)3.5 第五关:Embedding 模型的“维度静默降级”与向量一致性
DashScopeEmbeddings调用text-embedding-v4时,文档说“默认 1024 维度”。但如果你没显式指定dimension=1024,它实际返回的向量长度可能是 768(v1/v2 模型的默认值)。为什么?因为langchain-community的DashScopeEmbeddings类,在初始化时没有强制校验模型与维度的匹配关系,而是依赖 DashScope 服务端的兜底行为——当请求的维度不支持时,服务端静默返回低维向量,且不报错。
后果?RAG 检索时,你用text-embedding-v4生成的 query 向量是 1024 维,但数据库里存的 document 向量是 768 维(因初始化时漏了dimension参数),余弦相似度计算直接崩溃。
铁律写法(维度必须显式声明):
from langchain_community.embeddings import DashScopeEmbeddings # ✅ 正确:显式声明 dimension,且与模型名匹配 embeddings = DashScopeEmbeddings( model="text-embedding-v4", dimension=1024, # 必须!必须!必须! dashscope_api_key=clean_key ) # 验证向量维度 test_text = "LangChain 是什么?" vec = embeddings.embed_query(test_text) print(f"✅ Embedding 向量维度: {len(vec)} (预期: 1024)") # ❌ 错误示范(不要这样写) # embeddings = DashScopeEmbeddings(model="text-embedding-v4") # 漏 dimension,风险极高这五关,每一关都来自线上事故的血泪总结。它们不是“可选项”,而是 LangChain 接入百炼时的必经之路。跳过任何一关,你得到的都不是“能跑”,而是“随时会崩”。
4. Agents 实战:从 Hello World 到生产可用的客服对话引擎
“Agents” 是 LangChain 里最炫也最易翻车的概念。网上教程清一色是initialize_agent(tools=[SearchTool, CalculatorTool], llm=llm),跑通What is LangChain?就结束。但真实业务呢?比如我们要做一个电商客服 Agent,能查订单、退换货、查物流,还要记住用户说过“我不想要红色”,下次推荐时自动过滤。这需要的不是玩具,而是能扛住 200 QPS、平均响应 < 1.2 秒、错误率 < 0.3% 的引擎。下面,我用一个真实迭代的案例,展示如何从零构建。
4.1 阶段一:Hello Agents —— 为什么你的第一个 Agent 总是“答非所问”
新建一个HelloAgents,代码如下:
from langchain.agents import initialize_agent, Tool from langchain.agents import AgentType from langchain_community.llms import Tongyi llm = Tongyi(model="qwen-plus", dashscope_api_key=clean_key) tools = [ Tool( name="OrderQuery", func=lambda x: f"订单 {x} 状态:已发货,预计明天送达", description="查询订单状态,输入:订单号" ) ] agent = initialize_agent( tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True ) print(agent.run("我的订单 12345 怎么样了?"))运行结果却常是:
Thought: I need to use the OrderQuery tool to check the order status. Action: OrderQuery Action Input: {"order_id": "12345"} # 注意!这里传的是 dict,但我们的 func 只接受 str Observation: TypeError: expected string or bytes-like object Thought: ...问题定位:ZERO_SHOT_REACT_DESCRIPTIONAgent 的Action Input默认是 JSON 字符串,但Tool.func接收的是 Python dict。类型不匹配,直接报错。
修复(统一输入为字符串):
def safe_order_query(input_str: str) -> str: """安全包装:无论输入是 str 还是 dict,都提取 order_id""" try: # 尝试解析 JSON import json data = json.loads(input_str) order_id = data.get("order_id", data.get("query", "")) except: # 直接当字符串处理 order_id = input_str.strip() if not order_id: return "请提供有效的订单号" return f"订单 {order_id} 状态:已发货,预计明天送达" tools = [ Tool( name="OrderQuery", func=safe_order_query, description="查询订单状态。输入:订单号(如 '12345')或 JSON 字符串(如 '{\"order_id\": \"12345\"}')" ) ]4.2 阶段二:Stateful Memory —— 让 Agent 记住“用户讨厌红色”
initialize_agent的 memory 是 stateless 的,每次调用都是新会话。客服场景必须记住上下文。LangChain 官方推荐ConversationBufferMemory,但它有个致命缺陷:把整个对话历史拼成一个长字符串塞给 LLM,Token 消耗爆炸。查 5 个订单,history 就超 2000 tokens,Qwen-Plus 的 32K 上下文很快见底。
生产方案:Hybrid Memory(混合记忆)
- 短期记忆(Last 3 轮):用
ConversationBufferWindowMemory(k=3),存最近交互; - 长期记忆(用户画像):用 Redis 存 key-value,如
user:12345:preference = {"color": "not_red", "size": "L"}; - 工具调用记忆:每次
OrderQuery返回后,自动提取关键信息(如“已发货”)存入 Redis,供下次LogisticsQuery调用。
import redis from langchain.memory import ConversationBufferWindowMemory from langchain.agents import AgentExecutor # Redis 连接(生产环境用连接池) r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True) class HybridMemory: def __init__(self, user_id: str, window_size: int = 3): self.user_id = user_id self.window_memory = ConversationBufferWindowMemory( k=window_size, memory_key="chat_history", return_messages=True ) def load_memory_variables(self, inputs: Dict[str, Any]) -> Dict[str, Any]: # 加载短期记忆 short_term = self.window_memory.load_memory_variables(inputs) # 加载长期记忆(用户偏好) long_term = r.hgetall(f"user:{self.user_id}:preference") or {} # 合并 return { **short_term, "user_preferences": long_term, "user_id": self.user_id } def save_context(self, inputs: Dict[str, Any], outputs: Dict[str, Any]): # 保存短期记忆 self.window_memory.save_context(inputs, outputs) # 保存长期记忆:从输出中提取偏好(示例规则) output_text = outputs.get("output", "") if "不喜欢红色" in output_text or "不要红色" in output_text: r.hset(f"user:{self.user_id}:preference", "color", "not_red") # 使用 memory = HybridMemory(user_id="12345") agent_executor = AgentExecutor( agent=agent, tools=tools, memory=memory, verbose=True, handle_parsing_errors=True # 关键!捕获 Action Input 解析错误 )4.3 阶段三:Production Ready —— 熔断、降级、监控三位一体
上线前,必须加三道保险:
- 熔断器(Circuit Breaker):当百炼 API 连续 5 次超时(>5s),自动切换到备用模型(如本地 Ollama 的 Qwen2-1.5B)或返回兜底话术。
- 降级策略(Fallback):Agent 执行失败时,不抛异常,而是调用
SimpleLLM(无 Tools 的纯文本模型)给出温和回复:“抱歉,暂时无法查询订单,请稍后再试。” - 监控埋点(Metrics):记录每次调用的耗时、Token 消耗、Tools 调用次数、错误类型,接入 Prometheus。
from pydantic import BaseModel from typing import Optional, Dict, Any import time import logging class AgentMetrics(BaseModel): user_id: str start_time: float end_time: float duration_ms: float input_tokens: int output_tokens: int tool_calls: int error_type: Optional[str] = None class ProductionAgentExecutor(AgentExecutor): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.logger = logging.getLogger("ProductionAgent") self.circuit_breaker = {"failures": 0, "last_failure": 0} def _run_with_metrics(self, user_id: str, input: str) -> Dict[str, Any]: metrics = AgentMetrics(user_id=user_id, start_time=time.time(), input_tokens=0, output_tokens=0, tool_calls=0) try: # 检查熔断 if self._is_circuit_open(): self.logger.warning(f"熔断开启,用户 {user_id} 使用降级模型") return self._fallback_response(input) # 执行 Agent result = self.invoke({"input": input}) metrics.end_time = time.time() metrics.duration_ms = (metrics.end_time - metrics.start_time) * 1000 # 提取 Token 和 Tools 信息(需自定义 LLM 包装器) if hasattr(self.agent.llm, "last_token_usage"): usage = self.agent.llm.last_token_usage metrics.input_tokens = usage.get("prompt_tokens", 0) metrics.output_tokens = usage.get("completion_tokens", 0) metrics.tool_calls = len(result.get("intermediate_steps", [])) return {"output": result["output"], "metrics": metrics.dict()} except Exception as e: metrics.end_time = time.time() metrics.duration_ms = (metrics.end_time - metrics.start_time) * 1000 metrics.error_type = type(e).__name__ self._handle_failure() self.logger.error(f"Agent 执行失败: {e}", exc_info=True) return {"output": self._fallback_response(input)["output"], "metrics": metrics.dict()} def _is_circuit_open(self) -> bool: now = time.time() if now - self.circuit_breaker["last_failure"] > 60: # 60 秒窗口 self.circuit_breaker["failures"] = 0 return self.circuit_breaker["failures"] >= 5 def _handle_failure(self): self.circuit_breaker["failures"] += 1 self.circuit_breaker["last_failure"] = time.time() def _fallback_response(self, input: str) -> Dict[str, str]: # 降级:用最简 LLM from langchain_community.llms import Tongyi fallback_llm = Tongyi( model="qwen-turbo", # 更快更便宜 dashscope_api_key=clean_key, temperature=0.1 ) return {"output": fallback_llm.invoke(f"请用一句话温和回复用户:{input}")} # 使用 prod_agent = ProductionAgentExecutor(agent=agent, tools=tools, memory=memory) result = prod_agent._run_with_metrics(user_id="12345", input="我的订单 12345 怎么样了?") print(f"✅ 响应: {result['output']}") print(f"📊 指标: {result['metrics']}")这套方案已在我们客户侧稳定运行 4 个月,日均处理 12 万次对话,P95 响应时间 1.18 秒,熔断触发 3 次(均为百炼服务端波动),无一次客诉。它证明:Agents 不是玩具,而是可工程化的生产力。
5. LangGraph:当你的 Agent 流程复杂到“状态机”都无法描述时
initialize_agent适合线性流程(思考→选工具→执行→返回),但真实业务常是网状的。比如客服场景:
- 用户说“我要退货”,需先查订单 → 若已签收,走退货流程;若未发货,走取消流程;
- 退货流程中,用户可能突然问“物流到哪了?”,需中断退货,查物流,再回到退货;
- 用户还可能说“等等,我换个地址”,需更新地址,再继续。
这种“状态可回溯、流程可打断、分支可嵌套”的需求,AgentExecutor的单链式设计撑不住。这时,LangGraph 是唯一解。
5.1 为什么 LangGraph 不是“LangChain 的升级版”,而是“另一个物种”
很多人混淆 LangGraph 和 LangChain。LangChain 是组件库(LLM、Tool、Retriever),LangGraph 是流程编排引擎(类似 Airflow、Prefect)。它用有向无环图(DAG)定义节点(Node)和边(Edge),每个 Node 是一个函数(可以是 LLM 调用、Tool 执行、条件判断),Edge 是函数的输出决定的跳转逻辑。
关键差异:
| 特性 | LangChain Agent | LangGraph |
|---|---|---|
| 状态管理 | 依赖外部 Memory(如 Redis) | 图内原生 State(Pydantic Model) |
| 流程控制 | 固定 REACT/PLAN 模式 | 完全自定义:条件分支、循环、并行、中断恢复 |
| 可观测性 | 日志分散 | 每个 Node 执行可打点、记录输入输出、耗时 |
| 调试难度 | 链式调用,错误定位难 | 图可视化,可单步执行任意 Node |
5.2 用 LangGraph 重构客服引擎:一个可落地的完整示例
目标:构建一个能处理“查订单→退货/取消→物流查询→地址更新”的客服图。
Step 1:定义 State(状态模型)
from typing import Annotated, Sequence, Literal, Dict, Any from langgraph.graph import StateGraph, END from langgraph.checkpoint.memory import MemorySaver from pydantic import BaseModel class CustomerState(BaseModel): user_id: str messages: Annotated[Sequence[Dict[str, str]], lambda x: x[-10:]] # 只存最后 10 条 order_id: str = "" current_intent: Literal["query", "return", "cancel", "logistics", "update_address"] = "query" address: str = "" last_tool_result: str = "" # 初始化图 workflow = StateGraph(CustomerState)Step 2:定义 Nodes(节点函数)
def node_query_order(state: CustomerState) -> Dict[str, Any]: """查询订单节点""" if not state.order_id: return {"current_intent": "query", "messages": [{"role": "assistant", "content": "请提供订单号"}]} # 调用百炼查订单(此处简化) result = f"订单 {state.order_id} 状态:已签收" return { "last_tool_result": result, "messages": [{"role": "assistant", "content": result}], "current_intent": "return" if "已签收" in result else "cancel" } def node_return_process(state: CustomerState) -> Dict[str, Any]: """退货流程节点""" return { "messages": [{"role": "assistant", "content": "已为您发起退货申请,请等待快递上门取件。"}], "current_intent": "logistics" # 下一步查物流 } def node_logistics_query(state: CustomerState) -> Dict[str, Any]: """查物流节点""" # 模拟调用物流 API logistics = "快递已发出,预计明天送达" return { "last_tool_result": logistics, "messages": [{"role": "assistant", "content": logistics}] } # 添加节点到图 workflow.add_node("query_order", node_query_order) workflow.add_node("return_process", node_return_process) workflow.add_node("logistics_query", node_logistics_query)Step 3:定义 Edges(边逻辑)
def route_after_query(state: CustomerState) -> Literal["return_process", "cancel_process", END]: """根据订单状态路由""" if state.current_intent == "return": return "return_process" elif state.current_intent == "cancel": return "cancel_process" else: return END def route_after_return(state: CustomerState) -> Literal["logistics_query", END]: """退货后是否查物流""" # 用户可能打断,所以检查最新消息 last_msg = state.messages[-1]["content"] if state.messages else "" if "物流" in last_msg or "到哪" in last_msg: return "logistics_query" return END # 设置边 workflow.set_entry_point("query_order") workflow.add_conditional_edges("query_order", route_after_query) workflow.add_conditional_edges("return_process", route_after_return) workflow.add_edge("logistics_query", END)Step 4:编译并运行(带持久化)
# 添加内存检查点(支持会话恢复) checkpointer = MemorySaver() app = workflow.compile(checkpointer=checkpointer) # 运行(模拟用户输入) config = {"configurable": {"thread_id": "12345"}} initial_state = CustomerState( user_id="12345", messages=[{"role": "user", "content": "我要退货,订单号 12345"}], order_id="12345" ) # 执行 for event in app.stream(initial_state, config, stream_mode="values"): if "messages" in event and event["messages"]: print(f"🤖 {event['messages'][-1]['content']}") # 输出: # 🤖 订单 12345 状态:已签收 # 🤖 已为您发起退货申请,请等待快递上门取件。 # 🤖 快递已发出,预计明天送达为什么这比 Agent 更可靠?
- 状态隔离:每个
thread_id对应独立 State,不会因并发请求互相污染; - 流程可控:
route_after_return函数可精确判断用户意图,而不是靠 LLM 猜; - 调试友好:
app.stream(..., stream_mode="debug")可看到每个 Node 的输入输出; - 扩展性强:新增“发票申请”节点,只需
add_node+add_edge,无需重构整条链。
LangGraph 不是“高级玩法”,而是当业务复杂度超过某个阈值后的必然选择。它把模糊的“Agent 思考”变成了清晰的“程序流程”,这才是工程化的正道。
6. 最后一点掏心窝子的经验:别迷信“最新”,要信“压测数据”
写完这篇,我翻出自己电脑里 12 个 LangChain 项目文件夹,最早的创建于 2023 年 3 月。那时langchain==0.0.315,ChatOpenAI还叫OpenAI,AgentType只有ZERO_SHOT_REACT_DESCRIPTION。现在langchain==0.1.20,AgentExecutor、LangGraph、Runnable全是新范式。但你知道最讽刺的是什么吗?我去年用老版本写的 RAG 服务,至今还在跑,P99 延迟 0.87 秒;而用最新LangGraph重构的同功能服务,压测时 P99 却是 1.42 秒——因为新版本默认启用了更多中间件、更严格的 Schema 校验、更复杂的 State 序列化。
所以,我最后想说的,不是技术细节,而是心态:
- 别被“最新”绑架。
langchain-community里ChatTongyi的源码,我一行行读过,它和langchain-openai的ChatOpenAI在核心 HTTP 调用上,90% 代码是