做智能客服这行最怕的不是模型答错,而是模型还没答呢,整个服务先被流量冲垮了。我有一年在电商大促期间值班,眼看着监控面板上的QPS从几十冲到几百,机器人回复从秒回变成几十秒超时,用户排队队列越滚越长,后台工单爆炸,最后所有流量只能切给人工客服,那场面实在不想经历第二次。
用LangChain搭一个能跑通文档问答的客服demo确实不难,难的是把它放到生产环境里扛住真实的并发压力。今天这篇东西,我把实际踩过的坑、用过的方案和最终沉淀下来的做法整理出来,核心围绕三个关键词展开:流控、排队、语义降级。如果你正准备用LangChain做智能客服,或者已经在线上被流量折磨过,这篇文章应该能帮你省掉不少弯路。
1. 高并发智能客服的流量从哪来,为什么要分层治理
1.1 一条客服请求在系统里经历了什么
一次典型的LangChain客服请求,链路大概是这样:用户消息进入会话管理器,然后交给检索器从向量库或知识库里召回相关文档,拼装Prompt之后调用大模型,最后把答案返回给前端。看起来简单,但每个环节都可能成为瓶颈。
我拆解过一个实际客服系统的瓶颈分布,主要卡在三个地方。第一是模型提供方的接口配额,OpenAI、通义、文心这类商用模型都有每分钟请求数或Token数限制,这是最硬的墙。第二是中间数据链路,包括向量检索的QPS、知识库接口的响应速度,如果企业内部知识库挂在OpenWiki或者自建Wiki系统上,检索接口一旦扛不住,整个链路都会等在那里。第三是会话状态管理,多轮对话要把历史记录存下来,Redis和内存的读写也会在高并发下出现延迟抖动。
很多团队一开始只盯着大模型本身,觉得只要模型够聪明客服就靠谱,结果压测时模型还没打满,前面这些中间环节先被拖崩了。所以做高并发客服系统的第一步,不是优化Prompt,而是把整条链路的瓶颈都摸清楚。
1.2 不能把压力全部转嫁给大模型
有一种很自然的想法:既然大模型那么强,所有请求都发给它不就行了?真这么干,线上大概三天就会出事故。原因有三个。
成本扛不住。大模型按Token计费,一次客服对话可能要消耗几千Token,并发一高,账单几天就能跑出一个可观的数字。延迟扛不住。商用模型接口响应时间通常在一到三秒,高峰期会更高,如果所有请求都直接打到模型上,用户等十几秒都算正常。稳定性更扛不住。模型提供方的限流策略是黑盒的,你没法控制它什么时候返回429或者5xx,一旦触发限流,所有请求都会跟着失败。
理解这事可以用收费站的例子:收费站如果不控制进入高速的车流量,所有车都挤在入口,反而谁也别想走。大模型就是高速路,它的通行能力是固定的,调用方必须自己做好流量整形,把请求均匀地放进去,而不是一股脑全塞给它。
1.3 分层治理的整体设计
既然瓶颈分散在不同环节,治理也必须分层做。我最终沉淀下来的方案分成三层:
| 层级 | 核心组件 | 职责 |
|---|---|---|
| 网关层 | Sentinel / Nginx | 接口级QPS限制、热点参数限流、并发线程数控制 |
| 应用层 | 令牌桶 + Redis队列 + 降级策略 | 平滑流量、排队削峰、LLM不可用时语义降级 |
| 模型层 | LangChain Retry + Fallback | 控制超时、重试退避、多模型切换 |
网关层解决的是“别让太猛的流量打进来”,应用层解决的是“打进来的流量怎么排队处理”,模型层解决的是“模型挂了怎么兜底”。三层各管各的事,不要混在一起。
这套设计和LangChain本身能力强弱关系不大,核心是把架构思维想清楚。很多教程只教你怎么调LangChain的API,但到了生产环境,真正决定系统能不能扛住高并发的,恰恰是这些工程化能力。
2. 核心机制一:令牌桶流控与LangChain侧并发治理
2.1 令牌桶算法到底在干什么
流控最常用的算法是令牌桶。它的思路不复杂:系统以固定速率往桶里放令牌,桶的容量有限,每个请求进来要拿一个令牌才能继续处理,桶里没令牌了就直接拒绝或排队等待。
我用停车场栏杆来类比这个逻辑。栏杆后面有一块能停N辆车的缓冲区,每秒钟有一辆车从这个缓冲区放出去。如果一下子来了十辆车,缓冲区放不下,后面的车就只能在外面的路上排队。缓冲区就是令牌桶,每秒放行的车辆数就是速率限制。
实际实现时可以用现成的库,不用重复造轮子。在Python项目里我常用pyrate-limiter或者自己写个简单的异步令牌桶,核心代码不超过五十行:
import asyncio import time class TokenBucket: def __init__(self, rate: float, capacity: int): self.rate = rate # 每秒放入令牌数 self.capacity = capacity # 桶容量 self.tokens = capacity self.updated_at = time.monotonic() async def acquire(self): while True: now = time.monotonic() self.tokens = min( self.capacity, self.tokens + (now - self.updated_at) * self.rate ) self.updated_at = now if self.tokens >= 1: self.tokens -= 1 return await asyncio.sleep(0.01)为什么强调用令牌桶而不是简单的计数器?因为令牌桶允许短时间内的突发流量,只要桶里有存货就能放行,适合客服这种偶尔来一波集中咨询的场景。计数器限流则是一刀切,超过阈值直接拒绝,体验上生硬很多。
这里顺便提一个我看过很多遍的类比:CAN总线协议里的流控帧有个参数叫STmin,表示发送端两个连续帧之间的最小间隔。它的本质也是限制发送速率,防止接收方处理不过来。流控不管在哪个技术栈里,核心思路都是相通的:控制发送方的速度,保护接收方的处理能力。
2.2 网关层:参考Sentinel做接口级流量治理
如果项目是基于Spring Cloud或者微服务架构,网关层我很推荐参考阿里巴巴开源的Sentinel来做流量治理。Sentinel的流控规则支持QPS阈值、并发线程数控制,还有三种流量控制效果:快速失败、预热模式、排队等待。
以智能客服的对话接口为例,我会对/api/chat配置这样的规则:QPS阈值为200,超过阈值的请求进入排队等待模式,超时时间设置为5秒。核心配置大概这样:
# sentinel flow rule 配置 resource: /api/chat grade: 1 # 1表示按QPS count: 200 # 每秒钟最多放行200个请求 controlBehavior: 2 # 2表示排队等待 maxQueueingTimeMs: 5000 # 排队最长等待5秒这个配置解决的是“量”的问题。如果某次大促瞬间涌入几千个请求,Sentinel会按照每秒200的速率放行,多余请求在网关层排队,5秒内排不到就直接返回“当前咨询人数较多”。这样后面的LangChain服务和模型接口都不会被打爆。
不过Sentinel只做接口级治理还不够。客服场景有个特殊点:同一个用户连续发送的消息最好能单独限制。比如一个用户可能因为操作失误疯狂点发送按钮,一秒钟发了几十条重复消息,这种流量如果也放过去,既浪费模型额度又把队列塞满。
Sentinel的热点参数限流正好解决这个问题,可以对请求参数中的userId做热点维度限流,每秒最多5次:
resource: /api/chat grade: 1 count: 5 paramIdx: 0 # 请求参数中userId所在的位置2.3 应用层:LangChain调用侧的并发控制
网关层挡完一轮之后,到了应用层还要再做一次并发控制。原因很简单:网关限流保护的是接口本身,但LangChain内部还会调用检索服务、向量数据库、大模型API,这些下游资源的并发上限可能比网关QPS限制还要低。
我在应用层用的是最原始的信号量方案。把同时进入LangChain链路的请求数限制在固定数量,比如20个,剩下的请求先在内存队列里等。这样做的目的是确保同一时刻不会有过多的请求去抢大模型的连接池。
import asyncio from langchain_openai import ChatOpenAI from langchain.schema import HumanMessage llm = ChatOpenAI( model="gpt-4o-mini", temperature=0.2, request_timeout=15, # 单次请求超时15秒 ) sem = asyncio.Semaphore(20) async def chat_with_llm(user_input: str) -> str: async with sem: response = await llm.ainvoke([HumanMessage(content=user_input)]) return response.content信号量的数量怎么定?我一般根据模型接口允许的并发数和单次请求的平均响应时间反推。如果模型侧允许100个并发,单次请求平均1秒,那信号量设成80左右,留出20的余量给突发延迟。不要贪心设满,否则一旦模型响应变慢,所有请求会同时积压在这80个信号量上,最后集体超时。
重试策略也要在这个层面做。LangChain自带重试机制,但默认策略在高并发场景是灾难。我建议自己控制重试:
- 重试最多3次,避免无限重试
- 使用指数退避,每次重试间隔翻倍,并加随机抖动
- 只在网络错误、超时、限流(HTTP 429)时重试
- 业务逻辑错误不要重试,重试多少次都不会成功
import random import asyncio async def chat_with_retry(llm, messages, max_retries=3): for attempt in range(max_retries): try: return await llm.ainvoke(messages) except Exception as e: if attempt == max_retries - 1: raise wait_time = 2 ** attempt + random.uniform(0, 0.5) await asyncio.sleep(wait_time)3. 核心机制二:排队策略与用户体验的平衡
3.1 用排队论估算等待时长
流量超过处理能力之后,排队是必然的。问题是怎么让排队的体验不那么糟糕。至少用户要能知道“我前面还有多少人、大概等多久”,而不是看到一片空白或者直接报错。
排队论的M/M/c模型特别适合分析这种情况。把客服请求看成顾客,每个LangChain worker看成服务台,请求按泊松过程到达,服务时间服从指数分布。核心参数就三个:到达率λ、单个worker的服务率μ、worker数量c。
我写过一个非常简单的Python脚本,用来估算不同参数下的排队情况:
import math def erlang_c(c, a): # Erlang C 公式:所有服务台都忙的概率 sum_terms = sum(a**k / math.factorial(k) for k in range(c)) term = (a**c / math.factorial(c)) * (c / (c - a)) return term / (sum_terms + term) # 参数:每秒20个请求 # 每个worker每秒能处理5个请求(平均服务时间200ms) lam = 20 # 到达率 mu = 5 # 服务率 c = 6 # worker数量 a = lam / mu rho = lam / (c * mu) # 服务强度 print(f"服务强度 rho = {rho:.2f}") if rho < 1: c_prob = erlang_c(c, a) wq = c_prob / (c * mu - lam) # 平均等待时间 print(f"等待概率 = {c_prob:.2%}") print(f"平均等待时间 = {wq*1000:.0f} 毫秒") else: print("系统已过载,排队时间会无限增长")这个脚本跑出来的数字很有参考意义。当服务强度ρ小于0.6时,等待概率很低,系统很空闲;当ρ超过0.8,等待概率快速上升;一旦ρ超过1,意味着请求到达的速度超过系统处理能力,队列会无限增长,无解。
所以排队策略的第一步是监控这个ρ值。如果服务强度长期高于0.8,就该扩容worker了。
3.2 基于Redis的排队队列实现
很多人一说到排队就想到上MQ,Kafka、RabbitMQ一股脑都用上。其实对于智能客服这种场景,Redis的List结构完全够用,而且更轻量。
我的实现思路是酱紫:请求先入Redis List,worker从List里BRPOP取任务,同时用一个Hash记录每个用户的排队信息,前端轮询时查询自己在队列中的位置。
import redis import json import time r = redis.Redis(host='localhost', port=6379, decode_responses=True) def enqueue_request(user_id, session_id, content): task = { "user_id": user_id, "session_id": session_id, "content": content, "timestamp": time.time(), } # LPUSH + BRPOP 实现队列 r.lpush("chat:queue", json.dumps(task)) # 记录该用户排队信息 r.hset(f"chat:queue:info:{user_id}", mapping={ "status": "waiting", "enqueue_time": time.time(), }) def get_queue_position(user_id): # 扫描队列,统计在该用户之前的人数 queue_items = r.lrange("chat:queue", 0, -1) position = 0 for item in queue_items: task = json.loads(item) if task["user_id"] == user_id: return position position += 1 return NoneRedis List天然支持LPUSH和BRPOP组合,BRPOP会阻塞等待,worker空闲时才会取到任务,不会空转。最重要的是Redis的LLEN命令可以O(1)获取队列长度,前端拿这个数据做排队进度展示非常方便。
排队策略里有个细节容易被忽略:超时机制。用户排了十秒还没处理到,前端应该主动提示“当前排队人数较多,是否继续等待”,而不是让用户一直等下去。我会在用户排队超过5秒时启动降级逻辑,超过10秒直接返回一个可用的兜底答案或者转人工入口。
3.3 会话黏滞与优先级处理
智能客服和普通HTTP接口不一样,同一个用户的多轮问题是强关联的。用户问“我的订单发货了吗?”,机器人回答完,用户追问“那退货运费呢?”,这需要知道上文提到的订单号。如果把同一个用户的多个请求分散到不同worker处理,上下文就断了。
解决方法是会话黏滞。每个请求进入队列时带上session_id,消费端的worker在处理前先判断当前是否已有其他worker在处理同一个session,如果是,就把请求重新塞回队列或者等待。
实际项目里我用Redis的分布式锁来做会话粒度互斥:
def process_task(user_id, session_id): lock_key = f"lock:session:{session_id}" # 尝试获取锁,过期时间设置为60秒 acquired = r.set(lock_key, "1", nx=True, ex=60) if not acquired: # 该会话正在被其他worker处理,重新入队 r.lpush("chat:queue", json.dumps({ "user_id": user_id, "session_id": session_id, "retry": True, })) return try: # 从Redis中读取该session的历史对话 history = r.lrange(f"history:{session_id}", 0, -1) # 调用LangChain处理 answer = process_with_langchain(history, user_input) # 更新历史 r.rpush(f"history:{session_id}", json.dumps({ "role": "user", "content": user_input })) r.rpush(f"history:{session_id}", json.dumps({ "role": "assistant", "content": answer })) finally: # 处理完释放锁 r.delete(lock_key)优先级问题也不能忽视。普通用户和VIP用户排在一个队伍里,VIP用户一定会投诉。我用Redis ZSet做优先级队列,分数就是优先级加上排队序号,VIP用户分数低,优先被取走。
实操上我建议优先级不要分太多级别,两三级就够:VIP、普通、异常用户。分级太多会让普通用户永远等不到服务,反而引发大规模投诉。
4. 核心机制三:语义降级,让服务“能答而不是报错”
4.1 降级不是简单返回系统繁忙
流控和排队解决的是“流量进来怎么处理”的问题,但还有一类场景需要单独设计:模型不可用了怎么办。
这里说的“不可用”不只是大模型接口挂掉,还包括超时、限流、网络抖动。很多团队的降级策略非常简单粗暴——直接返回“系统繁忙,请稍后再试”。这种降级在用户眼里等于没处理,问题没解决还要再问一遍,体验极差。
我早期踩过的坑就是这里。有一次模型供应商限流,线上所有请求降级成固定文案,结果客服工单量在那个小时暴增了三倍,因为用户得不到答案,全都转人工了。
后来我把降级思路换成了“尽力作答”:即使不能调大模型,也要想办法给用户一个尽量有用的答案。降级不是断崖式切断,而是分级别逐步降,每一级都尽量保留一部分服务能力。
4.2 LangChain的with_fallbacks怎么用
LangChain自带的with_fallbacks是天然为这种情况设计的。它的作用是为一个调用链绑定多个备用实现,主链失败时自动尝试备链。
在客服场景里我配置过三级降级链路:
from langchain_openai import ChatOpenAI from langchain_ollama import ChatOllama # 主模型:商用模型,效果最好 primary_llm = ChatOpenAI( model="gpt-4o-mini", temperature=0.2, request_timeout=10, ) # 一级降级:本地部署的开源模型,效果稍差但完全可控 fallback_llm = ChatOllama( model="qwen2.5:7b", temperature=0.2, request_timeout=10, ) # 二级降级:基于知识库检索的规则回答 from langchain_core.runnables import RunnableLambda def rule_based_answer(query: str) -> str: # 从知识库检索最相似FAQ,返回预置答案 faq = search_faq(query) if faq: return faq["answer"] return "当前人工客服繁忙,已记录您的诉求,稍后短信回复。" chat_chain = ( primary_llm .with_fallbacks([fallback_llm, RunnableLambda(rule_based_answer)]) )这段代码的意思是:先试商用大模型,失败后试本地模型,再不行就走FAQ知识库检索。每降一级,回答质量差一些,但至少用户在多数场景下能得到答案而不是一句“系统繁忙”。
要注意的是,with_fallbacks触发条件是主链抛异常,判断哪些异常需要触发降级这点要格外小心。业务侧的自定义异常不应该触发模型降级,否则一些本来不应该重试的用户请求会把降级链路也打满。
4.3 RunnableBranch做意图路由降级
除了模型降级,还有一层更重要、也更容易被忽略的降级——意图层的降级。大模型是负责理解用户意图的核心,如果大模型不可用,我们可以退而求其次,用规则和关键词匹配来理解意图,再走对应的处理逻辑。
LangChain的RunnableBranch可以实现这个效果。它类似于编程语言里的switch-case,根据条件把请求路由到不同的处理链:
from langchain_core.runnables import RunnableBranch, RunnableLambda def classify_intent_with_rules(query: str) -> str: # 基于规则的意图分类,不需要大模型 if "发货" in query or "物流" in query: return "track_order" if "退款" in query or "退货" in query: return "refund" if "人工" in query or "客服" in query: return "human" return "general_faq" # 规则意图识别,作为降级方案 fallback_classifier = RunnableLambda(classify_intent_with_rules) # 各意图对应的处理链 order_chain = RunnableLambda(lambda query: query_order_status(query)) refund_chain = RunnableLambda(lambda query: query_refund_policy(query)) faq_chain = RunnableLambda(lambda query: search_faq_answer(query)) human_chain = RunnableLambda(lambda query: "正在为您转接人工客服,请稍候...") # 意图路由分支 branch = RunnableBranch( (lambda x: x["intent"] == "track_order", order_chain), (lambda x: x["intent"] == "refund", refund_chain), (lambda x: x["intent"] == "human", human_chain), faq_chain # 默认分支 ) def get_intent_and_route(query: str): try: # 优先用LLM识别意图 intent = llm_intent_classify(query) except Exception: # 大模型不可用时,用规则分类 intent = classify_intent_with_rules(query) return branch.invoke({"intent": intent, "query": query})这套规则降级方案能覆盖多少场景?以电商客服为例,大概60%的常见问题都集中在物流查询、退款政策、发票开具、活动咨询这几个意图上,写十几条关键词规则就能覆盖大部分。对于剩下的长尾问题,降级后引导用户留下联系方式转人工处理,比抛出一句“系统繁忙”强太多。
我还做了一层更细的降级:多轮上下文缩减。当模型超时不可避免时,尝试把历史对话从完整列表缩减到最近一轮或两轮,再发给模型。很多客服问题的回答只需要依赖最近一轮对话,历史太长反而拖慢响应速度。
5. 实操:一个可复现的高并发客服骨架
5.1 核心环境与依赖准备
上面讲了很多方案,最终要落到代码上。我整理了一个精简但完整的高并发客服骨架,包含流控、排队、降级三部分,可以直接参考改造。
依赖清单大致如下:
pip install langchain langchain-openai redis pyrate-limiter这个骨架用的是LangChain框架配合FastAPI提供HTTP接口,Redis负责队列管理,pyrate-limiter做令牌桶限流。不依赖太重的基础设施,本地就能跑起来验证逻辑。
5.2 流控排队降级完整链路代码
下面是核心代码框架。为了不拖沓,我直接给出可运行的简化版本:
import asyncio import json import time import redis from fastapi import FastAPI, Request from pyrate_limiter import BucketFullException, Duration, Rate, Limiter, MemoryListStorage from langchain_openai import ChatOpenAI from langchain.schema import HumanMessage app = FastAPI() r = redis.Redis(host="localhost", port=6379, decode_responses=True) # 1. 流控:每秒钟最多放行30个请求,桶容量50 limiter = Limiter( Rate(30, Duration.SECOND), storage=MemoryListStorage(), bucket_name="chat_bucket", ) sem = asyncio.Semaphore(20) # 2. LLM实例 llm = ChatOpenAI( model="gpt-4o-mini", temperature=0.2, request_timeout=10, ) # 3. 降级备用模型 from langchain_ollama import ChatOllama fallback_llm = ChatOllama(model="qwen2.5:7b", temperature=0.2) # 业务降级函数 def business_degrade(user_id: str, query: str) -> dict: if "物流" in query or "发货" in query: return {"answer": "请提供订单号,我为您查询物流信息。", "degraded": True} if "退款" in query or "退货" in query: return {"answer": "您可以在订单页面申请售后,审核周期1-3个工作日。", "degraded": True} return {"answer": "当前咨询量较大,已记录您的需求,人工客服将尽快回复。", "degraded": True} # 核心处理函数 async def handle_message(user_id: str, content: str) -> dict: # 流控:令牌桶拦截 try: limiter.try_acquire(user_id) except BucketFullException: return { "answer": "当前咨询人数较多,请稍后重试。", "queue_info": None, } # 排队:入Redis队列 task = {"user_id": user_id, "content": content, "ts": time.time()} r.lpush("chat:queue", json.dumps(task)) # 模拟worker消费(真实项目中这里是独立进程/线程组) async with sem: queue_item = r.rpop("chat:queue") if not queue_item: return {"answer": "系统繁忙,请稍后重试。", "queue_info": None} task = json.loads(queue_item) query = task["content"] try: # 主链路:LangChain + LLM response = await llm.ainvoke([HumanMessage(content=query)]) return {"answer": response.content, "degraded": False} except Exception: try: # 降级链路1:本地模型 response = await fallback_llm.ainvoke([HumanMessage(content=query)]) return {"answer": response.content, "degraded": True} except Exception: # 降级链路2:规则回答 return business_degrade(user_id, query) @app.post("/api/chat") async def chat(request: Request): body = await request.json() user_id = body.get("user_id") content = body.get("content") if not user_id or not content: return {"error": "missing params"} result = await handle_message(user_id, content) return result这个骨架把本章前面讲的三块机制串起来了:令牌桶限制进入频率,Redis List做排队,线程信号量限制并发,LLM异常时依次降级到本地模型和规则回答。放到真实项目中,worker消费逻辑应该独立成一个进程池,而不是和HTTP请求处理耦合在一起,但整体思路是一致的。
5.3 容量评估与压测观察点
部署之前一定要做容量估算。有个简单公式可以先用起来:并发窗口数 = QPS × 平均响应时间(秒)。
举个例子,目标QPS是50,单次请求平均耗时1秒,那系统至少要能承受50个并发请求同时处理。如果平均耗时2秒,并发窗口就是100。这个数算出来之后,再看你的worker数量够不够支撑,不够就扩容或者调低目标QPS。
压测时重点观察四个指标:
| 指标 | 健康阈值 | 超标处理 |
|---|---|---|
| P95响应时间 | 小于3秒 | 增加worker数量或优化检索链路 |
| 排队长度 | 不超过队列容量80% | 开启更严格的流控 |
| 降级比例 | 小于5% | 超过5%说明容量不足,需要扩容 |
| 错误率 | 小于0.1% | 检查超时和重试策略 |
我见过很多团队压测只关心QPS能不能扛住,忽略降级比例。其实降级比例更能反映真实体验——如果10%的请求都降级成规则回答了,用户感受到的“智能客服”基本就是个人工智障,口碑会崩得很快。
6. 常见问题与排查技巧实录
6.1 重试风暴:重试不当反而拖垮系统
重试是LangChain和高并发场景里最容易埋雷的地方。我之前在压测时遇到过现象:模型接口明明已经返回限流错误,但所有请求都在同一时间点重试,导致限流更加严重,形成恶性循环。
后来排查发现,默认重试策略是高并发场景的灾难。不加退避、不加重试上限、不做随机抖动,就会产生“重试风暴”。我的对策很简单:重试上限3次,指数退避从2秒开始,每次加0到0.5秒的随机抖动,把同时重试的请求冲散。
另外要区分哪些错误值得重试。网络超时、连接断开、限流错误可以重试;模型返回的业务错误比如ContextLengthExceeded、InvalidRequest重试一万次也没用,必须直接走降级。
6.2 超时设置与流控参数容易忽视
超时设置如果全链路不统一,会出很隐蔽的问题。前端请求超时5秒,网关超时10秒,LangChain调用大模型的超时也设10秒,那前端已经超时断开了,后端还在默默等待,资源白白浪费。
正确的做法是超时时间形成梯度:前端超时最短,网关次之,应用调用模型的最长。比如前端3秒,网关5秒,模型调用8到10秒。这样请求在每一层都能被有效切断,不会出现无效请求一直占用资源的情况。
流控参数也值得检查。回到之前提过的STmin类比,一旦设置过小,发送端会因为发送过快导致接收端丢帧,网络协议层面同样需要限速。应用层流控参数一定要结合服务端的实际处理能力来定,不要凭感觉随便填。
6.3 LangChain还是LangGraph,别一上来就上重框架
这个话题在很多LangChain面试题里出现,但施工时很容易踩坑。简单单轮问答、文档检索、ChatPromptTemplate加一个LLM,用LangChain就够;多轮复杂对话、状态机流转、人工介入审核、需要循环和条件跳转的工作流,才值得上LangGraph。
早期我也犯过“技术越复杂越好”的毛病,客服系统第一版就用上了LangGraph的状态图,结果维护成本极高。后来发现大部分用户请求是单轮或双轮,根本不需要图编排,用LangChain的链式调用就够了。技术选型要根据业务复杂度来,不是越重越好。
6.4 RunnableParallel不是“高并发”的银弹
面试里常见的一个问题:LangChain里的RunnableParallel能提高并发能力吗?它能做的是让多个互不依赖的步骤并行执行。比如同时做知识库检索和意图识别,两个操作可以在同一个请求里并行跑,缩短单次响应时间。但它是“单个请求内部并行”,而不是“多个请求同时处理”。
高并发是系统级的QPS能力,要靠架构层面的水平扩容、流控、排队来保证,一个RunnableParallel改变不了这个事实。把单请求从串行改成并行可以减少响应时间,却不会让系统单位时间能处理的请求数变多。这个问题理清楚,很多架构设计上的误区就能避开。
最后分享一点个人体会。很多人以为智能客服的核心是大模型够不够聪明,实际做下来发现,真正决定体验的反而是工程上的细节:排队时用户能不能看到进度,模型挂了能不能给出兜底答案,重试会不会引发雪崩。先把流控、排队、降级这套地基打扎实,再上LangChain做智能增强,系统才能稳稳跑住业务。能跑在生产环境里的客服系统,靠的不是某一个炫酷模型,而是这套一层一层兜住的工程能力。