简介:这是一套基于PHP开发的微信在线AI客服系统源码,面向需要为企业微信搭建智能客服的中小团队与个人开发者,可解决7×24小时自动应答、人工转接与对话管理等实际需求。压缩包共38个文件,以31个PHP源码文件为主体,另含说明文档、配置示例与少量前端页面资源,整体约20.57MB,结构上按服务层、AI逻辑、对话管理与后台配置等模块拆分,便于二次开发与参数调整。系统支持文本对话、图片分析、视频分析等交互方式,并内置对话管理、人工转接、咨询提醒等高级功能,同时提供配置文件方便设置回复模板与对话策略。目前已有65人学习下载,适合希望快速搭建可定制智能客服平台、研究PHP客服系统架构的开发者参考借鉴。
1. 微信在线AI客服系统到底在解决什么问题
很多团队做微信生态的客服,第一反应是接个第三方SaaS,结果发现客户消息里夹着订单号、手机号、售后图片,数据全落在别人服务器上,老板一问「能不能私有化」就卡住了。2026年这波「微信在线AI客服系统源码」的需求,本质就是要把大模型对话能力、微信消息通道、企业知识库这三样东西捏在自己手里,跑在自己的服务器上。它适合两类人:一是手里有微信小程序或公众号、每天咨询量在几百到几千条的中小团队;二是做交付的集成商,需要一套能改、能贴牌、能对接客户已有CRM的底座。源码方案的核心价值不是「免费」,而是你能看到每一条消息从微信服务器进来、经过意图识别、命中知识库、调用大模型、再回到用户手机上的完整链路,出问题时有得查。
2. 微信消息通道怎么接:公众号、小程序与客服消息的选型
2.1 三种接入方式的边界与选择依据
微信侧能拿到用户消息的入口主要有三个,选错了后面全是返工。公众号(服务号)走的是微信服务器推送模式,用户在对话框发消息,微信把XML或JSON推到你的回调URL,你必须在5秒内响应,否则微信重试三次然后放弃。小程序客服消息类似,但用户是从小程序内的客服按钮进来,消息体结构不同。企业微信则是另一套API,适合内部员工和外部客户混合的场景。
| 接入方式 | 消息到达形式 | 响应时限 | 适合场景 | 主要限制 |
|---|---|---|---|---|
| 公众号服务号 | 服务器推送XML/JSON | 5秒 | 对外客服、菜单交互 | 需认证服务号,模板消息受限 |
| 小程序客服 | 服务器推送JSON | 5秒 | 小程序内咨询 | 需用户主动点客服按钮 |
| 企业微信 | 回调+主动调用API | 5秒 | 内部+外部混合 | 需企业认证,配置复杂 |
我一般建议:如果客户主要在小程序里下单,就选小程序客服;如果还要做菜单、推模板消息,公众号服务号更顺手。两者可以同时接,用同一个消息路由层做分发。
2.2 回调URL的验证与消息解密
微信推送的消息默认是加密的,用的是AES-256-CBC,密钥在公众号后台配置。验证回调URL时,微信会发一个GET请求带signature、timestamp、nonce、echostr四个参数,你需要用token做SHA1校验后原样返回echostr。这一步翻车最多的是token填错或者服务器时间不同步,导致签名对不上。
# wechat_callback.py import hashlib import time from flask import Flask, request, make_response app = Flask(__name__) WECHAT_TOKEN = "your_token_here" # 公众号后台配置的Token def check_signature(signature, timestamp, nonce): """微信签名校验:token、timestamp、nonce字典序排序后SHA1""" arr = sorted([WECHAT_TOKEN, timestamp, nonce]) sha1 = hashlib.sha1("".join(arr).encode("utf-8")).hexdigest() return sha1 == signature @app.route("/wechat", methods=["GET", "POST"]) def wechat(): if request.method == "GET": # 回调URL验证 signature = request.args.get("signature", "") timestamp = request.args.get("timestamp", "") nonce = request.args.get("nonce", "") echostr = request.args.get("echostr", "") if check_signature(signature, timestamp, nonce): return make_response(echostr) return make_response("signature error", 403) # POST消息处理见下一节 return make_response("success")这段代码的关键点:WECHAT_TOKEN必须和公众号后台「服务器配置」里的Token完全一致,大小写敏感。sorted排序是微信规定的字典序,不是按参数名长度。返回echostr时不要加任何额外字符,否则验证失败。如果一直提示「token验证失败」,先检查服务器时间,date命令看是否和标准时间差超过几分钟。
2.3 消息体解析与5秒响应的工程处理
微信推送的消息是XML格式(公众号)或JSON(小程序),包含FromUserName(用户openid)、ToUserName(你的公众号原始ID)、MsgType(text/image/event)、Content(文本内容)等字段。5秒响应是硬限制,但大模型生成一条回复通常要2到8秒,所以必须做异步:收到消息后立刻返回「success」空串,然后把消息丢进队列,由后台worker调用AI生成回复,再通过客服消息接口主动推给用户。
# 消息入队,立即返回success import json import redis r = redis.Redis(host="localhost", port=6379, db=0) @app.route("/wechat", methods=["POST"]) def handle_message(): xml_data = request.data # 解析XML省略,假设已得到user_msg字典 user_msg = parse_wechat_xml(xml_data) # 丢进Redis队列,worker异步处理 r.lpush("wechat_msg_queue", json.dumps(user_msg)) return make_response("success") # 必须5秒内返回这里有个血泪经验:如果你在回调里直接调大模型,微信等不到响应会重试,用户会收到重复回复。用Redis做队列是最轻量的方案,worker用brpop阻塞读取,处理完调客服消息接口。注意客服消息接口有48小时窗口限制,用户最后一次互动后48小时内才能主动推送,超了就只能等用户再发消息。
3. AI客服大脑怎么搭:意图识别、知识库与模型调用
3.1 意图识别用规则还是模型
意图识别的目的是判断用户这句话是要查订单、问售后、还是纯闲聊。小团队我建议先用规则+关键词匹配,比如「订单」「物流」「退款」命中售后意图,「你好」「在吗」命中闲聊。规则的好处是可控、可解释、零延迟。当规则覆盖率达到70%以上再考虑上模型,用BERT微调或者直接调大模型做few-shot分类。
# intent_router.py 规则意图识别 INTENT_RULES = { "order_query": ["订单", "物流", "发货", "快递", "到哪了"], "after_sale": ["退款", "退货", "换货", "坏了", "质量问题"], "human_service": ["人工", "转人工", "真人", "客服"], "chitchat": ["你好", "在吗", "谢谢", "再见"] } def detect_intent(text): for intent, keywords in INTENT_RULES.items(): if any(kw in text for kw in keywords): return intent return "unknown" # 兜底走大模型参数说明:INTENT_RULES的key是意图标识,value是关键词列表。any做的是子串匹配,中文不需要分词也能用。unknown兜底很重要,不要硬塞进某个意图,交给大模型做开放域回答更稳。如果发现「人工」被误判成「order_query」,检查关键词顺序,把human_service的优先级提前。
3.2 知识库检索:向量库选型与分块策略
知识库是AI客服能不能答准的核心。常见做法是把产品文档、FAQ、售后政策切成小块,用embedding模型转成向量存进向量库,用户提问时先检索最相关的3到5块,拼进prompt让大模型基于这些内容回答。向量库选型上,小规模(几千条)用Chroma或FAISS本地跑就够,上百万条再考虑Milvus或Qdrant。
分块策略直接影响召回率。我一般按语义段落切,每块300到500字,重叠50字。切太碎会丢上下文,切太大检索精度下降。下面是一个用LangChain做分块和入库的示例:
# build_knowledge_base.py from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import Chroma # 1. 读取文档 with open("faq.txt", "r", encoding="utf-8") as f: raw_text = f.read() # 2. 分块:按段落切,块大小400,重叠50 splitter = RecursiveCharacterTextSplitter( chunk_size=400, chunk_overlap=50, separators=["\n\n", "\n", "。", "!", "?", " "] ) chunks = splitter.split_text(raw_text) # 3. 向量化并存入Chroma embeddings = HuggingFaceEmbeddings(model_name="BAAI/bge-small-zh-v1.5") vectorstore = Chroma.from_texts( texts=chunks, embedding=embeddings, persist_directory="./chroma_db" ) vectorstore.persist() print(f"入库完成,共{len(chunks)}块")参数说明:chunk_size=400是经验值,中文场景下400字大约对应一个完整问答对。chunk_overlap=50保证跨块语义不断裂。separators的顺序很重要,优先按双换行切,再按单换行,最后按句号,这样能尽量保持段落完整。bge-small-zh-v1.5是中文小模型,CPU也能跑,适合预算有限的团队。如果检索结果总是不相关,先检查分块是不是把问答对切散了,把chunk_size调到600试试。
3.3 大模型调用与Prompt模板设计
大模型调用层要处理三件事:拼prompt、调API、解析返回。Prompt模板决定了回答风格和准确性。我一般用这个结构:系统角色 + 检索到的知识 + 用户问题 + 输出约束。
# llm_client.py import openai SYSTEM_PROMPT = """你是一个微信在线客服助手。请根据以下知识库内容回答用户问题。 如果知识库中没有相关信息,请如实说「这个问题我需要转人工确认」,不要编造。 回答要简洁,控制在100字以内。""" def build_prompt(user_question, retrieved_docs): context = "\n---\n".join(retrieved_docs) return f"""知识库内容: {context} 用户问题:{user_question} """ def call_llm(user_question, retrieved_docs): prompt = build_prompt(user_question, retrieved_docs) response = openai.ChatCompletion.create( model="gpt-4o-mini", # 或替换为国产模型 messages=[ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": prompt} ], temperature=0.3, # 低温度保证回答稳定 max_tokens=200 ) return response.choices[0].message.content参数说明:temperature=0.3是客服场景的推荐值,太高会胡说,太低会死板。max_tokens=200防止模型长篇大论,微信消息太长体验差。retrieved_docs一般取top 3,太多会稀释关键信息还增加token成本。如果模型总是说「转人工」,检查检索是不是没召回,把相似度阈值调低或者增加召回数量。
4. 源码落地时最容易翻车的五个地方
4.1 现象:用户收到重复回复 → 原因:5秒超时重试 → 解决:异步队列+消息去重
微信服务器在5秒内没收到响应会重试,最多三次。如果你在回调里同步调大模型,必然超时。解决方法是收到消息立刻返回success,把处理逻辑丢给后台worker。但光这样还不够,微信重试的消息可能重复入队,需要在入队前用MsgId做去重,Redis的setnx设一个60秒过期的key就能挡住。
4.2 现象:AI回答和知识库对不上 → 原因:分块把问答切散 → 解决:按语义边界切+重叠
FAQ文档里一个问答对通常是「问题:xxx\n回答:xxx」的结构,如果按固定字数切,很可能把问题和回答切到两个块里,检索到问题块但回答块没召回。解决方法是自定义分隔符,把「问题:」作为切分点,保证每个块包含完整问答。或者用RecursiveCharacterTextSplitter时把\n\n优先级提到最高。
4.3 现象:客服消息推送失败 → 原因:48小时窗口过期 → 解决:记录最后互动时间+模板消息兜底
微信客服消息接口规定,用户最后一次发消息后48小时内才能主动推送。如果用户昨天问了问题,你今天才处理完想回复,接口会返回45015错误。解决方法是数据库记录每个openid的last_interact_time,推送前判断是否超窗。超窗了只能用模板消息(需用户订阅)或者等用户再发消息。
4.4 现象:向量检索慢 → 原因:每次请求都重新加载模型 → 解决:模型常驻内存
HuggingFaceEmbeddings如果每次检索都实例化,加载模型要好几秒。正确做法是在应用启动时初始化一次,全局复用。Flask里可以放在before_first_request或者用单例模式。Chroma的persist_directory也要确保只加载一次,不要每次请求都from_texts。
4.5 现象:大模型API费用失控 → 原因:无缓存+无长度限制 → 解决:加语义缓存+max_tokens
相同问题反复问,每次都调API是浪费。可以在Redis里做一层语义缓存:把用户问题embedding后算余弦相似度,超过0.95就直接返回缓存答案。另外max_tokens必须设,不然模型可能生成几百字。还有个小技巧:把系统prompt和知识库内容做前缀缓存,部分模型厂商支持prompt caching,能省不少钱。
5. 把AI客服接进现有系统的三个进阶技巧
5.1 用函数调用让AI直接查订单
纯问答的客服价值有限,用户问「我的订单到哪了」,AI应该能直接调你的订单接口查物流,而不是让用户自己去看。用大模型的function calling能力可以实现:定义query_order(order_id)函数,模型判断用户意图后返回函数名和参数,你的代码执行查询再把结果喂回模型生成自然语言回复。
# function_calling.py tools = [{ "type": "function", "function": { "name": "query_order", "description": "根据订单号查询物流状态", "parameters": { "type": "object", "properties": { "order_id": {"type": "string", "description": "订单号"} }, "required": ["order_id"] } } }] # 模型返回tool_calls后,执行本地函数 def handle_tool_call(tool_call): if tool_call.function.name == "query_order": args = json.loads(tool_call.function.arguments) return query_order_from_db(args["order_id"])关键点:description要写清楚,模型靠它判断什么时候调。required里的参数模型必须提供,但用户可能没给订单号,这时候模型会追问,体验反而更好。执行完函数后要把结果作为role: tool的消息追加到对话里,再调一次模型生成最终回复。
5.2 多轮对话的上下文管理
微信客服天然是多轮的,用户可能先问「订单」,再问「什么时候到」,再问「能改地址吗」。如果每轮都独立处理,AI会丢失上下文。做法是用openid做key,在Redis里存最近5轮对话历史,每次请求把历史拼进prompt。但要注意token长度,超过模型上限要截断最早的轮次。
| 存储方案 | 优点 | 缺点 | 适用规模 |
|---|---|---|---|
| Redis List | 读写快,天然支持过期 | 内存成本高 | 日活<1万 |
| MySQL | 持久化,可分析 | 读写慢 | 日活>1万 |
| 内存字典 | 零依赖 | 重启丢失 | 测试环境 |
我一般用Redis List,lpush新消息,ltrim保留最近10条,expire设7天。这样既控制内存又保证上下文够用。
5.3 人工接管与AI的平滑切换
AI不是万能的,用户说「转人工」或者AI连续两次回答「不知道」,就应该切到人工。实现上用一个状态字段标记会话是bot还是human,人工接管后AI不再自动回复,客服在后台看到消息手动回。切回AI可以设一个超时,比如人工30分钟没说话自动切回bot。
# session_manager.py def should_transfer_to_human(session): if session.get("intent") == "human_service": return True if session.get("unknown_count", 0) >= 2: return True return False def update_session(openid, intent, answer): key = f"session:{openid}" r.hincrby(key, "unknown_count", 1 if answer == "不知道" else 0) r.hset(key, "intent", intent) r.expire(key, 3600)这套逻辑跑下来,人工接管率能控制在15%以内,大部分标准问题AI自己就消化了。最后说个我自己的习惯:每次上线新知识库,先拿历史聊天记录跑一遍离线测试,看召回率和准确率,别直接上生产。这个后悔药我吃过一次,半夜被客户电话叫醒的滋味不好受。希望帮到你。
本文还有配套的精品资源,点击获取