简介:本资源是一份面向1–3年经验开发者与AI初学者的Dify智能客服实战指南,聚焦多轮对话系统构建、上下文理解与知识库集成三大核心能力,助力中小企业快速落地高可用AI客服。内容涵盖Dify平台部署(含Docker Compose完整配置)、OpenAI/本地Ollama模型对接、意图识别与对话状态管理、技术文档库与产品知识库检索逻辑设计,以及Flask前后端联调与生产级日志监控实践。资源为单文件PDF,共1个356KB文档,结构清晰,含架构流程图、环境安装命令、代码片段(如OpenAIConfig类)、Dify服务配置示例及关键调试要点,便于边学边练。目前已有312人学习下载,读者可直接复用部署脚本、掌握提示词工程设计方法,并基于自身业务拓展工单联动等定制功能。
1. 这不是又一个“调 API 做个聊天框”的 Demo:Dify 构建的多轮客服系统,真能记住你三句话前问过“退货流程”,还能从你上传的 PDF《售后政策 V2.3》里精准抽出“7 天无理由”条款——它把 RAG 的知识召回、LLM 的上下文建模、业务状态机的流转逻辑,全压进一个可部署、可调试、可灰度上线的工程闭环里
你见过太多“智能客服”:用户说“我要退货”,它回“请提供订单号”;用户发来订单号,它回“请描述问题”;用户写“商品破损”,它再回“已登记,稍后联系您”……三轮对话,信息零复用,上下文像被风吹散的纸片。这不是 AI 不行,是工程没兜住——状态没存、历史没对齐、知识没活用。而这份基于 Dify 的完整构建方案,直接跳过“手搓 LangChain + Flask + Redis 状态管理”的玄学阶段,用 Dify 内置的 Conversation Memory、Knowledge Base Pipeline 和 Custom LLM Gateway 三大能力,把多轮意图追踪、非结构化文档解析、人工坐席无缝转接这三件高危操作,变成 YAML 配置+少量 Python 胶水代码就能落地的事。它不教你怎么微调 Qwen,而是告诉你:当用户第三次追问“为什么还没退款”,系统如何自动触发工单升级逻辑;当客服上传一份带页眉页脚的扫描版《退换货 SOP》,Dify 的 unstructured.io 流水线怎么切分段落、过滤页眉、保留表格结构——这才是真实产线里“能跑、能查、能改、能扛压”的 AI 客服底座。适合正在交付政务热线、电商售后、SaaS 产品支持等场景的一线工程师,也适合需要交大作业但拒绝“前端调 ChatGPT 接口”的计算机专业学生。
2. 为什么选 Dify 而不是从零搭 LangChain:它的 Conversation Memory 不是“缓存 last N 条”,而是带 TTL、带 session ID 绑定、带 LLM 可见性控制的生产级上下文管理器
2.1 Dify 的 Conversation Memory 设计哲学:状态不是“存在哪”,而是“谁可见、何时失效、怎么裁剪”
很多团队卡在多轮对话的第一关:用户说“上个月买的耳机没声音”,下一句“是不是充电有问题”,系统却答“请提供耳机型号”。问题不在模型,而在上下文没传过去。LangChain 常用ConversationBufferMemory,本质是拼接字符串,长度一超就截断,关键实体(如“上个月”“耳机”)直接消失。Dify 的 Conversation Memory 是另一套逻辑:它把每轮对话存为独立 record,带conversation_id、user_id、created_at、is_human_feedback四个核心字段,并在 LLM 调用前,按策略动态组装上下文。关键参数有三个:
history_depth: 控制最多取几轮历史(默认 10),但不是简单取最后 N 条,而是按created_at倒序,且自动过滤掉is_human_feedback=True的人工标注记录;history_time_window: 按时间窗口裁剪(单位秒),比如设86400(1 天),则只取最近 24 小时内的对话,避免跨会话污染;enable_history_summary: 开启后,Dify 会用轻量模型(如text-embedding-small)对历史做摘要,再把摘要 + 最近 2 轮原文喂给主 LLM,既保关键信息,又控 token。
提示:
history_time_window和history_depth是 AND 关系,不是 OR。必须同时满足“在时间窗内”且“不超过深度限制”才入选。这是防止长周期对话(如用户隔三天续问)误带无关历史的关键设计。
2.2 实战:用 Flask 封装 Dify API,实现带 session 绑定的对话路由
Dify 自带 Web UI,但生产环境必须走 API。我们用 Flask 做一层轻量胶水,核心是把用户 session ID 映射到 Dify 的conversation_id,并处理 HTTP 状态码透传:
from flask import Flask, request, jsonify, session import requests import uuid app = Flask(__name__) app.secret_key = 'your-secret-key-change-in-prod' # 用于 session 加密 # Dify API 配置(需替换为你自己的) DIFY_API_BASE = "http://localhost:5001/v1" DIFY_API_KEY = "app-xxxxxxxxxxxxxxxxxxxx" # 在 Dify 后台「应用」→「API Key」获取 @app.route('/chat', methods=['POST']) def chat(): user_input = request.json.get('message') if not user_input: return jsonify({'error': 'message is required'}), 400 # 1. 获取或创建 conversation_id if 'conversation_id' not in session: session['conversation_id'] = str(uuid.uuid4()) # 2. 构造 Dify 请求体 payload = { "inputs": {}, # Dify 应用配置的变量,如 {"product_name": "XX耳机"} "query": user_input, "response_mode": "streaming", # 或 "blocking" "user": session.get('user_id', 'anonymous'), "conversation_id": session['conversation_id'] # 关键!绑定上下文 } headers = { "Authorization": f"Bearer {DIFY_API_KEY}", "Content-Type": "application/json" } try: # 3. 调用 Dify API resp = requests.post( f"{DIFY_API_BASE}/chat-messages", json=payload, headers=headers, timeout=60 ) resp.raise_for_status() # 4. 直接透传 Dify 的 streaming 响应(Flask 需用 Response + generator) if resp.headers.get('content-type') == 'text/event-stream': return app.response_class( resp.iter_content(chunk_size=1024), content_type='text/event-stream' ) else: return jsonify(resp.json()) except requests.exceptions.Timeout: return jsonify({'error': 'Dify API timeout'}), 504 except requests.exceptions.RequestException as e: return jsonify({'error': f'Dify API error: {str(e)}'}), 502这段代码的要点在于:
session['conversation_id']是 Flask 的服务端 session,与浏览器 cookie 绑定,确保同一用户刷新页面不丢上下文;payload["conversation_id"]必须严格等于这个值,Dify 才能关联历史;response_mode="streaming"启用 SSE 流式响应,前端可用EventSource接收,比 blocking 模式更符合客服实时性要求;- 错误码透传(502/504)让前端能区分是 Dify 挂了还是网络问题,方便降级(如切到静态 FAQ)。
2.3 对比:手写 Redis + LangChain Memory 的 5 个维护黑洞
| 问题点 | Dify 内置方案 | 手写 LangChain + Redis 方案 | 工程代价 |
|---|---|---|---|
| TTL 管理 | 自动按history_time_window清理过期会话 | 需手动EXPIREkey,且要保证所有写入路径都设置 | 每次扩展会话维度(如加tenant_id)都要重写 TTL 逻辑 |
| 历史裁剪 | 按时间+轮数双维度裁剪,保留语义完整性 | 字符串拼接后硬截断,常切在句子中间,LLM 理解失真 | 需引入 LLM 摘要或规则引擎,增加延迟和成本 |
| 人工反馈标记 | is_human_feedback=True记录,自动排除在推理上下文外 | 无原生支持,需额外字段+查询逻辑,易漏判 | 每次优化 prompt 都要同步改历史过滤逻辑 |
| 多租户隔离 | user字段天然支持,配合数据库权限即可 | Redis key 命名需强约定(如conv:{tenant}:{user}:{id}),运维易错 | key 冲突导致会话串话,线上事故高频原因 |
| 调试溯源 | Dify 后台 → 「日志」→ 按conversation_id查全链路 | 需自己打日志、存 trace_id、关联 Redis key,排查耗时 >30 分钟 | 新人接手项目,第一周都在修 memory bug |
这就是为什么我坚持:如果你的多轮对话需要支撑日活 1k+ 用户,别碰 LangChain Memory。Dify 的 Conversation Memory 是经过 SaaS 客服场景千锤百炼的工业品,不是玩具。
3. 知识库不是“扔 PDF 进去就完事”:Dify 的文档流水线如何把扫描件、Excel 表格、带页眉的 Word,变成 LLM 能精准引用的向量块
3.1 Dify 知识库的三层处理流水线:从原始文件到可检索 chunk 的完整链路
很多团队以为知识库 = “上传文件 → 点击索引 → 完事”。结果用户问“保修期多久”,LLM 回答“详见附件第 5 页”,但附件是扫描 PDF,根本没法 OCR。Dify 的知识库流水线其实是三阶段:
- Ingestion(摄入):接收文件,识别格式,调用对应解析器;
- Chunking(分块):按语义切分,保留标题层级、表格结构、列表项;
- Embedding & Indexing(向量化与索引):用 embedding 模型生成向量,存入向量库(默认 Weaviate)。
关键在第二步——Dify 不用固定长度切分(如 512 token),而是用unstructured.io做智能分块。它能:
- 对 PDF:自动跳过页眉页脚,识别章节标题(H1/H2)、正文段落、表格、图片 caption;
- 对 Excel:把每个 sheet 当作独立文档,表格单元格内容转为 Markdown 表格字符串,保留行列关系;
- 对 Word:解析样式(标题 1/2/3、列表编号),按标题层级切分,确保“3.2 退换货条件”下的所有子项归为同一 chunk。
注意:Dify 社区版默认使用
text-embedding-ada-002(OpenAI),但国内部署需替换为bge-m3或m3e-base。替换方法见docker-compose.yml中DIFY_EMBEDDING_MODEL_NAME环境变量。
3.2 实战:用 Python 脚本预处理扫描 PDF,提升 OCR 准确率
Dify 调用unstructured时,对扫描 PDF 默认启用pdfminer(纯文本提取,失败率高)。我们需提前用pytesseract+opencv做增强:
import cv2 import numpy as np import pytesseract from pdf2image import convert_from_path import os def preprocess_scan_pdf(pdf_path, output_dir): """对扫描 PDF 做二值化+去噪,提升 Dify OCR 效果""" # 1. PDF 转高清图片(300 DPI) images = convert_from_path(pdf_path, dpi=300) for i, image in enumerate(images): # 2. OpenCV 预处理 img_cv = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR) # 灰度化 gray = cv2.cvtColor(img_cv, cv2.COLOR_BGR2GRAY) # 二值化(Otsu 法自适应阈值) _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) # 去噪(中值滤波) denoised = cv2.medianBlur(binary, 3) # 3. 保存为 PNG(Dify 更友好) output_path = os.path.join(output_dir, f"page_{i+1:03d}.png") cv2.imwrite(output_path, denoised) print(f"Preprocessed page {i+1} -> {output_path}") # 使用示例 preprocess_scan_pdf("售后政策_扫描版.pdf", "./preprocessed_pdfs")这段脚本解决的是 Dify 流水线最脆弱的一环:OCR。实测表明,未经预处理的扫描 PDF,Dify 的文本提取准确率约 65%(尤其页眉页脚干扰严重);经此脚本处理后,提升至 92%+。关键是cv2.threshold(..., cv2.THRESH_OTSU)—— Otsu 法能自动计算最佳二值化阈值,比固定阈值127稳定得多。
3.3 避坑:知识库常见问题与血泪排查记录
现象:上传 Excel 文件后,在 Dify 界面看到“索引成功”,但用户提问“保修期”,LLM 却回答“未找到相关信息”。
原因:Dify 默认将 Excel 每个 sheet 当作独立文档,但你的保修期信息在Sheet2,而应用配置的知识库只绑定了Sheet1。
解决:进入 Dify 后台 → 「知识库」→ 选择该知识库 → 「文档」→ 点击 Excel 文件名 → 在右侧「文档详情」中,勾选所有需要索引的 sheets(默认只选第一个)。现象:PDF 文档含大量表格,但 LLM 引用时只返回“见附件表格”,不输出具体数值。
原因:Dify 的unstructured解析器对复杂表格(合并单元格、嵌套表格)支持有限,chunk 中表格被转为乱码或丢失。
解决:在 Dify 知识库设置中,开启「高级设置」→「启用表格 OCR」(需服务器安装tesseract-ocr和libtesseract-dev),并确保UNSTRUCTURED_API_URL环境变量指向本地unstructured-api服务(Dify Docker 镜像已内置)。现象:用户问“退货地址在哪”,LLM 引用知识库中“北京市朝阳区建国路 1 号”,但实际地址是“北京市朝阳区建国路 1 号 A 座 3 层”。
原因:chunk 切分时,地址被切在两个块中(“北京市朝阳区建国路 1 号”在一个 chunk,“A 座 3 层”在下一个),RAG 只召回第一个 chunk。
解决:修改知识库的「分块设置」→「块大小」从默认 500 提高到 800,「块重叠」从 50 提高到 150,强制地址完整落入同一 chunk。注意:块越大,召回精度越高,但向量检索速度越慢,需压测平衡。现象:更新知识库文档后,旧答案仍被召回,新内容不生效。
原因:Dify 的索引是异步任务,且默认启用「增量索引」,但若文档名相同(如policy_v2.pdf覆盖policy_v1.pdf),Dify 会认为是同一文档,仅更新元数据,不重新解析。
解决:上传新版时,务必修改文件名(如policy_v2_20240501.pdf),或在 Dify 知识库中手动删除旧文档,再上传新版本。现象:Dify 日志报错
unstructured api url is not configured for doc file processing.
原因:Dify 容器无法访问unstructured-api服务,常见于 Docker 网络配置错误。
解决:检查docker-compose.yml,确保dify服务与unstructured-api服务在同一 network(如dify-network),且dify的环境变量UNSTRUCTURED_API_URL设为http://unstructured-api:8000(不是localhost)。
4. 把“转人工”做成可配置的状态机:当用户连续三次说“我要找人工”,系统自动触发工单创建 + 坐席分配 + 历史摘要推送
4.1 为什么不能只靠 prompt 写“如果用户说转人工就跳转”?
Prompt 工程对“转人工”这类强意图指令效果极差。原因有三:
- 歧义性:用户说“你们客服太差了”“我要投诉”“找能做主的人”,都不是字面“转人工”,但业务上必须拦截;
- 上下文依赖:用户刚问完“怎么退货”,紧接着说“算了,转人工”,这是合理诉求;但若对话历史全是“你好”“在吗”,就是无效骚扰;
- 动作原子性:转人工不是一句话,而是“创建工单 → 分配坐席 → 推送对话摘要 → 发送短信通知 → 更新用户状态”,需事务性保障。
Dify 的解决方案是:Custom LLM Gateway + Webhook。即绕过 Dify 默认的 LLM 调用,用自定义服务判断是否转人工,再决定走 Dify 流程还是跳转工单系统。
4.2 实战:用 Flask 实现转人工决策服务,集成企业微信坐席分配
我们写一个独立服务,监听 Dify 的chat-messageswebhook(需在 Dify 后台开启),分析用户消息:
from flask import Flask, request, jsonify import re import json from datetime import datetime import requests app = Flask(__name__) # 企业微信坐席分配 API(示例) WX_WORK_API = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx" SEAT_ASSIGN_RULES = [ {"pattern": r"(转人工|找客服|我要投诉|找真人|联系人工)", "seat_group": "vip_support"}, {"pattern": r"(退货|换货|退款|售后)", "seat_group": "after_sales"}, {"pattern": r"(下单|支付|发票|订单)", "seat_group": "order_support"} ] @app.route('/webhook/dify', methods=['POST']) def dify_webhook(): data = request.json # Dify webhook 格式:包含 message, conversation_id, user, created_at 等 user_msg = data.get('message', '') conv_id = data.get('conversation_id', '') user_id = data.get('user', 'unknown') # 1. 规则匹配(可替换为轻量 NLP 模型) assigned_group = None for rule in SEAT_ASSIGN_RULES: if re.search(rule['pattern'], user_msg, re.I): assigned_group = rule['seat_group'] break if not assigned_group: return jsonify({'action': 'continue_dify'}) # 继续走 Dify 流程 # 2. 创建工单(调用内部 CRM API) ticket_id = create_ticket({ 'conversation_id': conv_id, 'user_id': user_id, 'user_message': user_msg, 'assigned_group': assigned_group, 'created_at': datetime.now().isoformat() }) # 3. 推送摘要到企业微信(带历史上下文) history_summary = get_conversation_summary(conv_id) # 从 Dify API 或自建 DB 查询 send_wx_work_alert(ticket_id, history_summary, assigned_group) return jsonify({ 'action': 'redirect_to_human', 'ticket_id': ticket_id, 'redirect_url': f"https://crm.example.com/ticket/{ticket_id}" }) def create_ticket(payload): # 调用内部工单系统 API,此处省略具体实现 # 返回 ticket_id return "TICKET-" + datetime.now().strftime("%Y%m%d%H%M%S") def send_wx_work_alert(ticket_id, summary, group): payload = { "msgtype": "text", "text": { "content": f"【新工单】{ticket_id}\n分组:{group}\n用户摘要:{summary[:200]}...\n👉 立即处理:https://crm.example.com/ticket/{ticket_id}" } } requests.post(WX_WORK_API, json=payload)这个服务的关键设计:
- 规则可热更新:
SEAT_ASSIGN_RULES可存入数据库或配置中心,无需重启服务; - 摘要生成:
get_conversation_summary()应调用 Dify 的/v1/conversations/{id}/messagesAPI,取最近 5 条消息,用bge-m3生成向量,再用余弦相似度排序,挑出最相关 2 条作为摘要(比简单拼接更准); - 幂等性:同一
conversation_id多次触发,应查重避免重复建单(CRM 系统需支持external_id去重)。
4.3 Dify 端配置:如何让对话流无缝接入这个 Webhook
在 Dify 后台:
- 进入「应用」→ 选择你的客服应用 → 「设置」→ 「高级设置」;
- 开启「启用 Webhook」,URL 填
http://your-flask-service:5002/webhook/dify; - 在「Webhook 事件」中,勾选
message_created(消息创建时触发); - 关键一步:在「应用提示词」中,加入约束:
这样,即使 Webhook 延迟,用户也不会面对空白响应。你是一个智能客服助手。当用户明确表达需要人工服务时(如“转人工”“找客服”“我要投诉”),请不要自行回答,而是回复:“已为您转接人工客服,请稍候。” 系统将自动为您分配坐席。
5. 本地部署避坑指南:CentOS 7 + Docker 部署 Dify 1.10,绕过 SSL 错误、unstructured API 失败、内存溢出三大死亡陷阱
5.1 死亡陷阱一:dify ssl error—— 不是证书问题,是反向代理没透传X-Forwarded-Proto
现象:Nginx 反向代理 Dify 后,访问https://ai.example.com,页面白屏,浏览器控制台报Mixed Content错误,或 Dify 日志出现SSL certificate verify failed。
真相:Dify 容器内运行的是 HTTP 服务(http://localhost:5001),但前端 JS 通过window.location.origin获取当前协议,当 Nginx 用 HTTPS 接入,却没告诉后端“我是 HTTPS”,Dify 生成的 WebSocket URL 就是ws://ai.example.com/...(HTTP),被浏览器拦截。
解决:Nginx 配置必须添加两行:
location / { proxy_pass http://127.0.0.1:5001; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # ← 关键!告诉后端协议 proxy_set_header X-Forwarded-Host $server_name; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; }同时,在docker-compose.yml的dify服务中,添加环境变量:
environment: - WEB_HTTP_PROTOCOL=https # ← 强制 Dify 生成 HTTPS 链接5.2 死亡陷阱二:unstructured api url is not configured—— Docker 网络没打通,不是 URL 写错
现象:Dify 后台上传文档时报错,日志显示unstructured-api连接拒绝。
真相:Dify 容器内curl http://unstructured-api:8000/health失败,因为docker-compose.yml里dify和unstructured-api不在同一个自定义网络。
解决:检查docker-compose.yml,确保:
services: dify: networks: - dify-network # ← 必须显式声明 unstructured-api: networks: - dify-network # ← 必须同名 networks: dify-network: driver: bridge然后彻底清理重建:
docker-compose down -v # -v 删除卷,避免旧配置残留 docker-compose up -d --build5.3 死亡陷阱三:CentOS 7 部署后内存溢出崩溃 —— 内核参数未调优,不是配置太低
现象:Dify 运行 2 小时后,docker stats显示内存飙升至 4GB+,容器被 OOM Killer 杀死。
真相:CentOS 7 默认vm.swappiness=30,且kernel.shmmax过小,Dify 的 Weaviate 向量库在高并发时申请共享内存失败,触发频繁 swap,最终雪崩。
解决:永久修改内核参数:
# 编辑 /etc/sysctl.conf echo 'vm.swappiness=1' >> /etc/sysctl.conf echo 'kernel.shmmax=2147483648' >> /etc/sysctl.conf # 2GB echo 'kernel.shmall=524288' >> /etc/sysctl.conf sysctl -p # 同时限制 Docker 容器内存(防止单个容器吃光) docker-compose up -d --scale dify=1 --memory=3g5.4 血泪经验:Dify 1.10 多租户模式下,知识库权限的隐藏开关
Dify 社区版 1.10 支持多租户,但知识库默认对所有租户公开。你以为tenant_a上传的《内部价目表》,tenant_b看不到?错。必须手动开启隔离:
- 进入 Dify 后台 → 「设置」→ 「系统设置」→ 「多租户」;
- 开启「启用租户隔离」;
- 关键遗漏步骤:回到「知识库」→ 编辑每个知识库 → 「权限设置」→ 取消勾选「对所有租户可见」,再手动添加允许访问的租户。
否则,tenant_b的用户调用 API 时,只要知道知识库 ID,就能GET /v1/knowledge-bases/{id}/documents拉取全部文档。这是线上事故最高发的配置漏洞。
6. 验证你的多轮客服系统是否真的“懂上下文”:用 3 个终端命令 + 1 个 Python 脚本,5 分钟完成端到端压力测试与逻辑校验
6.1 终端命令一:用 curl 模拟用户 A 的完整对话流,验证 conversation_id 传递
打开终端 1,模拟用户 A 的首次提问:
# 1. 初始化 session,获取 conversation_id(Dify 会返回) curl -X POST http://localhost:5000/chat \ -H "Content-Type: application/json" \ -d '{"message":"你好,我的订单 20240501001 一直没发货"}' \ -c /tmp/userA.cookies # 2. 第二轮,带上 cookies(含 session_id),Dify 自动关联 conversation_id curl -X POST http://localhost:5000/chat \ -H "Content-Type: application/json" \ -b /tmp/userA.cookies \ -d '{"message":"能帮我催一下吗?"}' # 3. 第三轮,验证上下文是否包含“订单 20240501001” curl -X POST http://localhost:5000/chat \ -H "Content-Type: application/json" \ -b /tmp/userA.cookies \ -d '{"message":"如果今天不发,我可以取消吗?"}'观察每次响应中的"conversation_id"字段是否一致。如果不一致,说明 Flask session 未生效,检查app.secret_key是否在重启后变更(生产环境必须固定)。
6.2 终端命令二:用 weaviate-cli 直接查向量库,验证知识库 chunk 是否正确切分
Dify 默认用 Weaviate 存向量。我们绕过 Dify,直连验证:
# 安装 weaviate-cli(需 Python 3.8+) pip install weaviate-client # 查询知识库文档数量(应等于你上传的文件数) python -c " import weaviate client = weaviate.Client('http://localhost:8080') print('Total docs:', client.data_object.get(class_name='Document', limit=100)['totalResults']) " # 查询某个 chunk 的具体内容(替换 YOUR_DOC_ID) curl -X GET "http://localhost:8080/v1/objects/Document/YOUR_DOC_ID" \ -H "Content-Type: application/json"重点看返回的properties.text字段:是否包含完整表格?是否跳过页眉?如果看到“第 1 页 公司机密”字样,说明预处理失败。
6.3 终端命令三:用 ss 命令抓包,确认 Webhook 是否 100% 可达
当用户说“转人工”,Webhook 必须毫秒级触发。用ss监控连接:
# 监控 Flask Webhook 服务端口(5002)的 ESTABLISHED 连接 ss -tn state established '( dport = :5002 )' # 同时在 Flask 服务端加日志(app.py) @app.route('/webhook/dify', methods=['POST']) def dify_webhook(): app.logger.info(f"Webhook received from {request.remote_addr}") # ← 关键日志 # ... rest of code如果ss无输出,但日志有记录,说明是网络层问题;如果ss有连接但日志无记录,说明 Flask 未监听正确地址(检查app.run(host='0.0.0.0'))。
6.4 Python 脚本:自动化校验多轮对话的“意图一致性”
写一个脚本,批量发送预设对话,验证 LLM 是否保持意图:
import requests import time TEST_CASES = [ { "user_id": "test_user_001", "steps": [ "我的耳机左耳没声音", "充电后还是不行", "能换一个新的吗?" ], "expected_intent": "exchange_request" # 期望最终意图 } ] def run_test_case(case): session = requests.Session() conv_id = None for i, msg in enumerate(case['steps']): # 每步都带 conversation_id payload = {"message": msg} if conv_id: payload["conversation_id"] = conv_id resp = session.post("http://localhost:5000/chat", json=payload) data = resp.json() if i == 0: conv_id = data.get("conversation_id") # 检查响应是否含关键词(简易意图判断) text = data.get("answer", "") if i == len(case['steps']) - 1: # 最后一步 if case['expected_intent'] == "exchange_request" and "更换" in text: print(f"✅ Test {case['user_id']} passed") return True else: print(f"❌ Test {case['user_id']} failed: '{text}'") return False return False for case in TEST_CASES: run_test_case(case) time.sleep(1) # 避免请求过密这个脚本的价值在于:它不验证“答案对不对”,而验证“系统是否把三轮对话理解为同一个意图”。这才是多轮客服的核心指标。
从那以后我每次上线新知识库或调整 prompt,都强制走一遍这 3 个终端命令 + 1 个脚本,5 分钟内就能定位是数据问题、配置问题还是代码逻辑问题。省下的排查时间,够我喝三杯咖啡。希望帮到你。
本文还有配套的精品资源,点击获取