前情
本身这篇文章是在学习了 Langchain 基础之后的一篇记录,由于一些原因一直未整理完成。看题目也知道,这个项目是在学习 Langchain 中期的一个实战:用以巩固 Langchain 基础。
需求背景
业务场景
电商客户反馈处理系统。
需求描述
某电商平台需要自动处理客户反馈,实现以下功能:
- 情感分析:判断用户反馈的情感倾向
- 问题分类:识别反馈中的问题类型
- 紧急程度评估:根据内容判断处理优先级
- 生成回复草稿:根据分析结果生成初步回复
核心技术
技术点
-
LangChain + LCEL 任务编排:
- 将复杂的自然语言处理流程(分析、分类、生成)抽象为一系列可组合的
Runnable。 - 通过
RunnableParallel、RunnableLambda、RunnablePassthrough等原语,构建了清晰的数据流管道(extract_chain->analysis_chain->generate_response)。
- 将复杂的自然语言处理流程(分析、分类、生成)抽象为一系列可组合的
-
“规则+大模型”的混合智能策略:
- 优先使用正则表达式 (
re.search(r'ORD\d{10}', text)) 匹配高度结构化的订单ID,仅在失败时降级调用大模型。
- 优先使用正则表达式 (
-
工程化设计:
-
容错与降级:
call_qwen_with_retry函数实现了指数退避(示例中是简单重试)的重试机制,并在最终失败时返回友好降级信息,保证了系统鲁棒性。 -
API与监控:通过FastAPI快速封装为RESTful服务 (
/process-feedback),并内置了处理耗时监控。注释部分展示了日志、批处理和缓存(如SQLiteCache)的增强方案,体现了从原型到生产的设计考量。 -
配置化思维:
optimized_qwen_call函数(注释中)展示了针对不同任务(情感分析、分类、生成)精细化调整大模型参数(temperature,max_tokens)的思路,这是优化效果与成本的关键。
-
项目架构
核心流程
graph TD
Start --> Stop
项目代码
模型生成
# 使用通义千问模型
qwen = ChatTongyi(
model_name="qwen-max",
temperature=0.2, # 控制创造性
max_tokens=2000, # 最大输出长度
streaming=False, # 关闭流式输出
enable_search=True # 启用联网搜索增强
)
带重试的模型调用
容错与降级机制,避免“重试风暴”。
# 辅助函数:带重试的模型调用 LLM
def call_qwen_with_retry(prompt, max_retries=3, retry_delay=2):
"""带错误重试的千问模型调用"""
for attempt in range(max_retries):
try:
response = qwen.invoke(prompt)
return response.content
except Exception as e:
print(f"模型调用失败 (尝试 {attempt + 1}/{max_retries}): {str(e)}")
time.sleep(retry_delay)
return "模型服务暂时不可用,请稍后再试。"
提取订单
# 提取订单ID ERP 订单系统
def extract_order_id(text: str) -> dict:
"""使用千问模型提取订单ID"""
prompt = f"""
你是一个电商订单处理专家,请从以下客户反馈中提取订单ID:
{text}
订单ID通常是"ORD"开头的10位数字组合。如果找不到订单ID,返回"NOT_FOUND"。
请严格按JSON格式返回结果:{{"order_id": "提取结果"}}
"""
try:
# 正则提取
match = re.search(r'ORD\d{10}', text)
return {"order_id": match.group(0) if match else "NOT_FOUND"}
except:
# 备选方案:大模型提取
result = call_qwen_with_retry(prompt)
# 尝试解析JSON
return json.loads(result.strip())
情感分类
- sentiment: 情感类型, 依赖 LLM
- confidence: 置信度, 依赖 LLM
- key_phrases: ["短语1", "短语2", "短语3"]
# 情感分类
def analyze_sentiment(text: str) -> dict:
"""使用千问模型进行情感分析"""
prompt = f"""
请分析以下客户反馈的情感倾向:
「{text}」
要求:
1. 判断情感类型:POSITIVE(积极)/NEUTRAL(中性)/NEGATIVE(消极)
2. 评估置信度(0.0-1.0)
3. 提取3个关键短语
返回JSON格式:
{{
"sentiment": "情感类型",
"confidence": 置信度,
"key_phrases": ["短语1", "短语2", "短语3"]
}}
"""
try:
# 调用千问模型
result = call_qwen_with_retry(prompt)
output_parser = JsonOutputParser()
result = output_parser.parse(result)
return result
except Exception as e:
print(f"情感分析失败: {e}")
return {
"sentiment": "NEUTRAL",
"confidence": 0.7,
"key_phrases": []
}
问题分类
# 问题分类
def classify_issue(text: str) -> dict:
"""使用千问模型进行问题分类"""
prompt = f"""
作为电商客服专家,请对以下客户反馈进行分类:
「{text}」
分类选项:
- 物流问题:配送延迟、物流损坏等
- 产品质量:商品瑕疵、功能故障等
- 客户服务:客服态度、响应速度等
- 支付问题:扣款异常、退款延迟等
- 退货退款:退货流程、退款金额等
- 其他:无法归类的反馈
要求:
1. 选择最相关的1-2个分类
2. 按相关性排序
返回JSON格式:{{"categories": ["分类1", "分类2"]}}
"""
try:
# 调用千问模型
result = call_qwen_with_retry(prompt)
output_parser = JsonOutputParser()
result = output_parser.parse(result)
return result
except Exception as e:
print(f"问题分类失败: {e}")
return {"categories": ["其他"]}
紧急状态
def assess_urgency(text: str) -> dict:
"""使用千问模型评估紧急程度"""
prompt = f"""
作为客服主管,请评估以下客户反馈的紧急程度:
「{text}」
评估标准:
- HIGH(高):包含"紧急"、"立刻"、"马上"或威胁投诉
- MEDIUM(中):表达强烈不满但无立即行动要求
- LOW(低):一般反馈或建议
返回JSON格式:
{{
"urgency": "紧急级别",
"sla_hours": 响应时限(小时),
"reason": "评估理由"
}}
"""
try:
result = call_qwen_with_retry(prompt)
output_parser = JsonOutputParser()
result = output_parser.parse(result)
# 确保数值类型
result["sla_hours"] = int(result["sla_hours"])
return result
except Exception as e:
print(f"紧急度评估失败: {e}")
return {
"urgency": "MEDIUM",
"sla_hours": 24,
"reason": "评估失败"
}
LLM定制化回复
def generate_response(data: dict) -> dict:
print(data)
"""使用千问模型生成定制化回复"""
prompt_template = """
你是一名资深电商客服专家,请根据以下分析结果生成客户回复:
### 客户反馈原文:
{feedback}
### 分析结果:
- 订单ID:{order_id}
- 情感倾向:{sentiment} (置信度:{confidence:.2f})
- 问题类型:{categories}
- 紧急程度:{urgency} (需在{sla_hours}小时内响应)
{key_phrases_section}
### 回复要求:
1. 根据情感倾向调整语气:
- 积极反馈:表达感谢,适当赞美
- 消极反馈:诚恳道歉,明确解决方案
2. 包含订单ID和问题分类
3. 明确说明处理时限和后续步骤
4. 长度100-150字,使用自然口语
5. 结尾询问是否还有其他问题
请直接输出回复内容,不需要额外说明。
"""
# 构建关键短语部分
key_phrases = data.get("key_phrases", [])
if key_phrases:
key_phrases_section = "- 关键要点:" + ",".join(key_phrases[:3])
else:
key_phrases_section = ""
# 填充模板
prompt = prompt_template.format(
feedback=data["original_feedback"],
order_id=data["order_id"],
sentiment=data["sentiment"],
confidence=data.get("confidence", 0.8),
categories="、".join(data["categories"]),
urgency=data["urgency"],
sla_hours=data["sla_hours"],
key_phrases_section=key_phrases_section
)
try:
response = call_qwen_with_retry(prompt)
# 添加紧急标识
if data["urgency"] == "HIGH":
response = f"[紧急] {response}"
return {
"final_response": response,
"assigned_team": data["categories"][0] if data["categories"] else "General",
"result":data
}
except Exception as e:
print(f"回复生成失败: {e}")
return {
"final_response": "感谢您的反馈,我们的团队将尽快处理您的问题。",
"assigned_team": "General"
}
LCEL处理链
extract_chain = RunnableParallel(
order_id=RunnableLambda(extract_order_id),
original_feedback=lambda x: x
)
用户行为分析
- RunnableParallel:由于三个分类没有依赖关系,这里使用并行处理
# 节省大模型处理的时间,使用并行处理
analysis_chain = RunnableParallel(
# 情感分析
sentiment=RunnableLambda(analyze_sentiment),
# 问题分类
categories=RunnableLambda(classify_issue),
# 紧急程度
urgency=RunnableLambda(assess_urgency)
)
# 步骤3: 组合完整流程
processing_chain = (
# 订单提取,正则表达式,异常才会用大模型
extract_chain
|
# 根据输入问题分类,情感分类,紧急程度
RunnablePassthrough.assign(
analysis=lambda x: analysis_chain.invoke(x["original_feedback"])
)
| {
"original_feedback": lambda x: x["original_feedback"],
"order_id": lambda x: x["order_id"]["order_id"],
"sentiment": lambda x: x["analysis"]["sentiment"].get("sentiment", "NEUTRAL"),
"confidence": lambda x: x["analysis"]["sentiment"].get("confidence", 0.8),
"key_phrases": lambda x: x["analysis"]["sentiment"].get("key_phrases", []),
"categories": lambda x: x["analysis"]["categories"]["categories"],
"urgency": lambda x: x["analysis"]["urgency"]["urgency"],
"sla_hours": lambda x: x["analysis"]["urgency"]["sla_hours"],
"urgency_reason": lambda x: x["analysis"]["urgency"].get("reason", "")
}
# 生成答案
| RunnableLambda(generate_response)
)
本地测试
见源码。
前端和部署
接下来通过 FastAPI 来部署服务,前端模板为 index.html。
app = FastAPI(title="电商客服系统")
主页
@app.get("/")
async def read_index():
return FileResponse("index.html")
反馈
processing_chain.invoke(request.content):前端调用 invoke 触发大模型回答
@app.post("/process-feedback")
async def process_feedback(request: FeedbackRequest):
try:
start = time.time()
result = processing_chain.invoke(request.content)
elapsed = time.time() - start
return {
"success": True,
"processing_time": f"{elapsed:.2f}s",
"result": result
}
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"处理失败: {str(e)}"
)
前端js代码
- 核心:处理后端数据
addMessage:添加 LLM 返回数据updateAnalysisPanel:更新前端页面
// 发送消息到后端API
async function sendMessage(message) {
showLoading();
try {
const response = await fetch(`${API_BASE}/process-feedback`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
content: message,
user_id: 'web_user'
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.success) {
addMessage(data.result.final_response, false);
updateAnalysisPanel(data.result);
} else {
throw new Error('API returned unsuccessful response');
}
} catch (error) {
console.error('Error:', error);
addMessage('抱歉,系统暂时无法处理您的请求,请稍后再试。', false);
analysisResults.innerHTML = '<div class="loading">分析失败,请重试</div>';
}
}
服务运行
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="localhost", port=8000)