Langchain入门到精通0x05:第一个实战-智能电商客服系统

0 阅读7分钟

前情

本身这篇文章是在学习了 Langchain 基础之后的一篇记录,由于一些原因一直未整理完成。看题目也知道,这个项目是在学习 Langchain 中期的一个实战:用以巩固 Langchain 基础。

需求背景

业务场景

电商客户反馈处理系统。

需求描述

某电商平台需要自动处理客户反馈,实现以下功能:

  • 情感分析:判断用户反馈的情感倾向
  • 问题分类:识别反馈中的问题类型
  • 紧急程度评估:根据内容判断处理优先级
  • 生成回复草稿:根据分析结果生成初步回复

核心技术

技术点

  1. LangChain + LCEL 任务编排

    • 将复杂的自然语言处理流程(分析、分类、生成)抽象为一系列可组合的Runnable
    • 通过RunnableParallelRunnableLambdaRunnablePassthrough等原语,构建了清晰的数据流管道(extract_chain-> analysis_chain-> generate_response)。
  2. “规则+大模型”的混合智能策略

    • 优先使用正则表达式​ (re.search(r'ORD\d{10}', text)) 匹配高度结构化的订单ID,仅在失败时降级调用大模型。
  3. 工程化设计:

    • 容错与降级call_qwen_with_retry函数实现了指数退避(示例中是简单重试)的重试机制,并在最终失败时返回友好降级信息,保证了系统鲁棒性

    • API与监控:通过FastAPI快速封装为RESTful服务 (/process-feedback),并内置了处理耗时监控。注释部分展示了日志、批处理和缓存(如SQLiteCache)的增强方案,体现了从原型到生产的设计考量。

    • 配置化思维optimized_qwen_call函数(注释中)展示了针对不同任务(情感分析、分类、生成)精细化调整大模型参数(temperature, max_tokens)的思路,这是优化效果与成本的关键。

项目架构

image.png

核心流程

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)

Run

image.png

image.png

源码

github