参考:用python实现自动回复QQ消息——不到60行-CSDN博客

一直想做个qq自动回复的机械人来自娱自乐,网上有成熟方案,但不愿冒封号的危险,偶然看到这篇文章,来了灵感

1、需要安装的模块

我测试的python版本是3.10,模块可以安装到conda虚拟环境里避免和其它项目冲突

1.pyautogui:让程序自动控制鼠标和键盘

2.pyperclip:复制剪贴板里的内容,向剪贴板写入内容

3.psutil:获取运行的进程信息

4.langchain:一个用于开发由语言模型驱动的应用程序的框架,提供了模块化的组件和可定制化的用例链

5.langchain-community

6.ollama 进行与ollama部署的模型的交互

7.numpy

8.faiss:用来检索本地知识库

2、下载Ollama并部署模型

本项目主要用到了两个模型:

qwen:7B 轻量大语言模型,一般的独显应该都能部署

bge-m3 对多语言、长文本等进行检索的模型

Ollama安装包应使用命令安装,不然会下到C盘:

下载安装文件后,用命令行安装即可:
.\OllamaSetup.exe /DIR = "D:\Ollama"

安装完毕后在Ollama设置中将模型存放位置改到D盘

下载模型命令:

ollama pull qwen:7B
ollama pull bge-m3

等待下载完成(可能有些慢)

3、 本地知识库处理

创建一个build_index文件,作用为生成索引和文本块,只需要每次更新本地知识库时运行一次即可

导入模块:

import os, glob, ollama, faiss, numpy as np
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import TextLoader

设置变量:

knowlodge_store = r"D:\information"#存放资料的txt文件所在文件夹
model = "qwen:7b" #语言模型
embed_model = "bge-m3"#识别查找的模型
index_file = "faiss.index"
loader_map = {
    "*.txt":  lambda p: TextLoader(p, encoding="utf-8"),
}#处理txt文件,可自行添加其他格式的处理工具

读取文件(演示代码里处理的知识库是txt文件,也可以自行改成其他格式的):

all_docs = []
for pattern, LoaderCls in loader_map.items():
    for file in glob.glob(os.path.join(knowlodge_store, pattern)):
        docs = LoaderCls(file).load()
        for d in docs:
            d.metadata["source"] = os.path.basename(file)
        all_docs.extend(docs)

切块并保存文本块列表文件:

texts = RecursiveCharacterTextSplitter(
    chunk_size=600,
    chunk_overlap=100
).split_documents(all_docs)
np.save("texts.npy" , texts)

向量化从而可以使faiss读取内容并处理

vecs = []
for t in texts:
    vec = ollama.embeddings(model=embed_model, prompt=t.page_content)["embedding"]
    vecs.append(np.array(vec, dtype=np.float32))
vecs = np.vstack(vecs)

保存faiss索引文件:

index = faiss.IndexFlatIP(vecs.shape[1])
index.add(vecs)
faiss.write_index(index, index_file)

文本块列表文件和索引文件生成在python文件同一文件夹下,如果更新了本地知识库,再次运行一遍上述代码即可。

  4、主代码

导入需要的模块:

import re
import time
import psutil
import pyautogui
import pyperclip
from langchain.chains import ConversationChain
from langchain_community.chat_message_histories import FileChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import HumanMessage
from langchain_ollama import ChatOllama
import pathlib
import os, ollama, faiss, numpy as np

定义需要的变量:

ollama的模型端口默认为http://localhost:11434

# 定义一些变量
system = (
    ""
)#人设,可自行填写或不写
qwen7b = ChatOllama(base_url="http://localhost:11434",model= "qwen:7b")
model = qwen7b
embed_model = "bge-m3"
QQ_path = r"D:\QQ\QQ.exe"
index_path = r"D:\PyCharm\pyproject\QQ\faiss.index"
loop = 2          # 每 2 秒询问一次
history_file = pathlib.Path(__file__).with_name("chat_history.json")#存储记忆以前的对话内容
history = FileChatMessageHistory(str(history_file))
index_file = "faiss.index"  # FAISS 索引文件
texts_file = "texts.npy"    # 文本块列表文件
qq_message_pic = r"D:\pics.png"

接下来是定义函数,首先是load_index_and_texts函数,作用是读取我们上面生成的本地知识库索引和文本块文件:

def load_index_and_texts():
    #加载 FAISS 索引
    index = faiss.read_index(index_file)
    #加载文本块列表
    texts = np.load(texts_file, allow_pickle=True)
    return index, texts.tolist()

然后是strip_between函数,作用是去除思考过程(如果有的话):

def strip_between(text: str, start: str, end: str) -> str:
    #去掉思考过程
    pattern = re.escape(start) + r".*?" + re.escape(end)
    return re.sub(pattern, "", text, flags=re.DOTALL).strip()

接着是两个用以判断qq是否运行的函数:

def is_qq_running() -> bool:
    #利用psutil判断 QQ.exe 是否存在进程
    return any(p.name().lower() == "qq.exe" for p in psutil.process_iter())

def open_qq():
    #如未运行则启动 QQ,并等待登录完成
    if not is_qq_running():
        print("未检测到 QQ,正在启动…")
        os.startfile(QQ_path)
        # 等待登录界面出现(可根据自己电脑调整)
        time.sleep(15)
    else:
        print("QQ运行中")

接着是两个用来判断是否有新消息的函数,原理是利用有新消息时qq图标变红,从而被pyautogui识别,有新消息时的图标图片请自行截图:

def locate_and_click(image_path: str, confidence=0.99, timeout=10):
    #在屏幕上找到qq图标并单击,需要opencv
    start = time.time()
    while time.time() - start < timeout:
        pos = pyautogui.locateOnScreen(image_path, confidence=confidence)
        if pos:
            center = pyautogui.center(pos)
            pyautogui.click(center)
            return center
        time.sleep(0.5)
    raise RuntimeError(f"未找到图片: {image_path}")

def find_qq():
    #找到qq并点击
    try:
        pos = locate_and_click(qq_message_pic, timeout=2)
        return pos
    except RuntimeError:
        return None

接着是获取并发送消息的函数,原理是qq电脑版打开时最新消息和发送框位置固定,也可以用opencv识别,我这里直接用了坐标,可自行修改:

def get_latest_msg() -> str:
    #打开会话后,通过坐标定位最新消息,通过Ctrl+C 复制聊天内容
    time.sleep(0.1)
    pyautogui.moveTo(563, 1154, duration=1)
    pyautogui.click()
    pyautogui.hotkey("ctrl", "c")
    time.sleep(0.2)
    text = pyperclip.paste()
    return text

def send_msg(text: str):
    #把text粘到剪切板
    pyperclip.copy(text)
    time.sleep(0.2)
    #找到对话框,发送ai回复的数据
    pyautogui.moveTo(1015, 1341, duration=1)
    pyautogui.click()
    pyautogui.hotkey("ctrl", "v")
    time.sleep(0.1)
    pyautogui.press("enter")

def close_session():
    """关闭当前聊天窗口(Ctrl+W)"""
    pyautogui.hotkey("ctrl", "w")
    time.sleep(0.5)

接着是对接ai的函数,原理是将剪切板中复制的消息发送给ai,ai会首先在本地资源库的文本块中查找,如果匹配度达到阈值,则将文本输入预设从而回答问题,如果未达到阈值,则直接调用大模型本身知识回答。

def ask(query: str, threshold: float = 0.45):
    index, texts = load_index_and_texts()
    index = faiss.read_index(index_path)
    q_vec = np.array(
        ollama.embeddings(model=embed_model, prompt=query)["embedding"],
        dtype=np.float32
    ).reshape(1, -1)
    #让FAISS返回分数+下标
    D, I = index.search(q_vec, 3)      #D是相似度分数
    best_score = D[0][0]                   #最高分
    #阈值判断:低于阈值 = 本地知识库没相关内容
    if best_score < threshold:
        prom = f""
    else:
        ctx = "\n".join(texts[i].page_content for i in I[0])
        prom = f"根据以下资料回答:\n{ctx}\n\n问题"

    prompt = ChatPromptTemplate.from_messages([
        ("system", system + prom),
        MessagesPlaceholder(variable_name="messages")
    ])
    chain = prompt | model
    with_memory = RunnableWithMessageHistory(
        chain,
        lambda session_id: history,
        input_messages_key="messages"
    )
    m = with_memory.invoke(
        {"messages": [HumanMessage(content=query)]},
        config={"configurable": {"session_id": "user_001"}}
    )
    return m

最后是运行流程的主要函数:

def main():
    open_qq()
    while True:
        try:
            #检测是否有消息,如没有则等待
            pos = find_qq()
            if not pos:
                time.sleep(loop)
                continue

            #找到发消息的人
            time.sleep(0.1)
            pyautogui.moveTo(271, 115, duration=1)
            pyautogui.click()

            #读取最新消息
            msg = get_latest_msg()
            if not msg:
                close_session()
                continue

            #调用本地大模型回答
            mes = ask(msg)
            word = mes.content

            #去掉思考过程
            word = strip_between(word, "<think>", "</think>")
            word = word.replace("\n" and "*","")
            reply = word

            #发送回复
            send_msg(reply)

            #最小化窗口
            pyautogui.hotkey("alt", "esc")

        except Exception as e:
            print("等待回复中", e)
            time.sleep(loop)

循环代码:

if __name__ == "__main__":
    time.sleep(loop)
    main()

运行时确保Ollama和QQ均开启,如果要调整速度,可以修改sleep的时间

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐