🎬 从“哑巴小球”到“话痨伙伴”

回顾一下前三篇的成果。

第一篇,我们给了小球物理灵魂——它会重力下落,会摩擦减速,会碰撞反弹,像个真正的纸团。

第二篇,我们给了小球一双眼睛——它能看见桌面上所有的窗口,撞到浏览器会弹开,落在文件夹上会停住。

第三篇,我们给了小球灵敏的耳朵——你戳它,它有反应;你扔它,它会滚;你按方向键,它会动。

但还有一个问题:它是个哑巴。

你跟它说“你好”,它没反应;你问它“今天天气怎么样”,它依然自顾自地弹跳。这哪里是伙伴,分明是个自闭的玩具。

一个真正的桌面小伙伴,应该是你问它答,你聊它应

那么问题来了:怎么让小球学会说话?怎么让它的回答像“小球”而不是冷冰冰的机器?

这就是本篇要解决的问题。

在这里插入图片描述


📦 拆解需求:什么叫“会聊天”?

同样,我们先拆解一下“会聊天”这个模糊目标:

🎯 理想场景 ⚙️ 技术需求
你打字问“你好”,它回答“你好呀” 接收输入 + 生成回复
它记得你刚才问过什么 对话历史管理
它的回答不超过20字,像个小球 角色设定 + 长度限制
关掉程序再打开,它还记得昨天聊过 持久化存储
API 可能失败,它得有礼貌地道歉 错误处理

拆开之后发现,原来“会聊天”是由一连串子任务组成的。


💡 思路:大模型 + 对话管理

让程序“说话”有几种做法:

做法 原理 优缺点
规则匹配 if “你好” then “你好呀” ❌ 太死板,聊不了三句
预定义语料库 随机返回预设句子 ❌ 聊久了重复,像个复读机
大模型(本方案) 调用 AI 接口生成回复 ✅ 灵活、智能、像真人
❌ 需要联网、有费用

显然,想让小球真的“会聊天”,得用大模型

那选哪个模型?BounceChat 接的是模力方舟的 DeepSeek-V3,原因很简单:

  • 每日免费 100 次——够你玩很久
  • 响应快——不像某些模型要等半天
  • 支持角色设定——可以让它扮演“弹跳小球”

在这里插入图片描述


🛠️ 该怎么做:一步步让小球开口说话

📁 第一步:先建个“大脑”模块

chat.py 里,我们定义了一个 ChatModel 类,专门处理所有和 AI 相关的事情。

from openai import OpenAI
import json
import os

class ChatModel:
    def __init__(self):
        """初始化大模型客户端"""
        self.client = OpenAI(
            base_url="https://ai.gitee.com/v1",
            api_key="你的API密钥",  # 记得改成自己的!
            default_headers={"X-Failover-Enabled": "true"},
        )
        
        # 聊天历史文件路径
        self.chat_log_file = os.path.join(
            os.path.dirname(os.path.abspath(__file__)), 
            "chat_history.json"
        )
        
        # 加载聊天历史
        self.chat_history = self.load_chat_history()

🗣️ 为什么单独建一个类?

这叫模块化设计——把所有和 AI 相关的代码集中到一起,主程序 (main.py) 只管调用。以后想换模型、改参数,都在一个地方改,不用满世界找代码。


🤖 第二步:给小球一个“人设”

要让小球回答得像小球,而不是像百科全书,需要给它设定一个角色

def load_chat_history(self):
    """加载聊天历史"""
    try:
        if os.path.exists(self.chat_log_file):
            with open(self.chat_log_file, 'r', encoding='utf-8') as f:
                return json.load(f)
    except Exception as e:
        print(f"加载聊天历史失败: {e}")
    
    # 默认系统消息:给小球设定角色
    return [
        {
            "role": "system",
            "content": "你是一个运行在用户电脑桌面上的弹跳小球,你回答内容的长度不能超过20字"
        }
    ]

这个 system 消息非常关键。它告诉大模型:

  1. 你是谁:弹跳小球
  2. 你怎么回答问题:长度 ≤20 字

💡 为什么是 20 字?

试出来的。太长像念课文,太短像敷衍。20 字刚好是“一句完整但简洁的话”的长度。

看几个真实回复的例子:

  • “你好,我是弹跳小球。”
  • “今天很开心!”
  • “我是小球,不看天气哦。”

是不是有内味儿了?


💬 第三步:发送消息并获取回复

def send_message(self, message):
    """发送消息给大模型并获取回复"""
    # 添加用户消息到聊天历史
    self.chat_history.append({
        "role": "user",
        "content": message
    })
    
    try:
        response = self.client.chat.completions.create(
            messages=self.chat_history,
            model="DeepSeek-V3_1",
            extra_body={
                "thinking": {"type": "disabled"},
                "top_k": 20,
            },
            stream=False,
            max_tokens=1024,
            temperature=0.6,
            top_p=0.8,
            frequency_penalty=1.1,
        )
        
        # 获取回复内容
        if len(response.choices) > 0:
            assistant_reply = response.choices[0].message.content
            
            # 添加助手回复到聊天历史
            self.chat_history.append({
                "role": "assistant",
                "content": assistant_reply
            })
            
            # 保存聊天历史
            self.save_chat_history()
            
            return assistant_reply
        else:
            return "抱歉,我没有理解你的问题,请重试。"
            
    except Exception as e:
        print(f"大模型调用失败: {e}")
        return "抱歉,我暂时无法回答你的问题,请稍后再试。"

这里面有几个重要参数:

参数 作用
temperature 0.6 控制随机性。越低越稳定,越高越天马行空
max_tokens 1024 最大回复长度(这里其实用不上,因为系统消息已经限制了)
frequency_penalty 1.1 避免重复说一样的话
top_k 20 每次从概率最高的 20 个词里选,保证不跑偏

🎯 调参心得

0.6 的 temperature 是“既稳定又有趣”的平衡点。太低像机器人,太高会胡言乱语。


💾 第四步:让小球有“记忆力”

关掉程序再打开,小球应该记得之前聊过什么。这就需要持久化存储

def save_chat_history(self):
    """保存聊天历史"""
    try:
        with open(self.chat_log_file, 'w', encoding='utf-8') as f:
            json.dump(self.chat_history, f, ensure_ascii=False, indent=2)
    except Exception as e:
        print(f"保存聊天历史失败: {e}")

每次聊天后,把整个 chat_history 存成 JSON 文件。下次启动时,load_chat_history 读回来。

📁 chat_history.json 长啥样?

[
  {
    "role": "system",
    "content": "你是一个运行在用户电脑桌面上的弹跳小球,你回答内容的长度不能超过20字"
  },
  {
    "role": "user",
    "content": "你好"
  },
  {
    "role": "assistant",
    "content": "你好,我是弹跳小球。"
  }
]

这样,每次调用 API 时,把整个历史传进去,大模型就知道上下文了。


🔗 第五步:把大脑接到身体上

现在 ChatModel 写好了,怎么把它和 main.py 里的小球连接起来?

BallWidget__init__ 里初始化:

from chat import ChatModel

class BallWidget(QWidget):
    def __init__(self):
        # ... 前面的代码 ...
        
        # 初始化大模型客户端
        self.chat_model = ChatModel()

然后在用户发送消息时调用:

def on_message_sent(self, message):
    """处理从输入框发送的消息"""
    print(f"收到消息: {message}")
    
    # 发送消息给大模型
    assistant_reply = self.chat_model.send_message(message)
    print(f"大模型回复: {assistant_reply}")
    
    # 更新气泡内容显示回复
    self.update_bubble_content(assistant_reply)

这里 on_message_sent 是由输入框的 WebChannel 回调触发的,我们会在下一篇详细讲。


🧪 第六步:测试对话

写个简单的测试代码(就在 chat.py 末尾):

if __name__ == "__main__":
    chat_model = ChatModel()
    response = chat_model.send_message("你好,你是?")
    print(f"回复: {response}")
    
    response2 = chat_model.send_message("今天天气怎么样?")
    print(f"回复: {response2}")

运行看看输出:

回复: 你好,我是弹跳小球。
回复: 我是小球,不看天气哦!

成了!小球真的会说话了。

在这里插入图片描述


🧠 深入思考:对话管理的几个坑

坑一:历史太长怎么办?

每次调用都把整个历史传进去,聊久了历史会越来越长,消耗 tokens 也变多。

解法:可以设置最大历史长度,比如只保留最近 10 轮对话。

MAX_HISTORY = 20  # 保留 20 条消息(10轮)

if len(self.chat_history) > MAX_HISTORY + 1:  # +1 是为了保留 system 消息
    # 保留 system 消息 + 最近 MAX_HISTORY 条
    self.chat_history = [self.chat_history[0]] + self.chat_history[-MAX_HISTORY:]

坑二:角色设定被冲掉怎么办?

有时候聊着聊着,小球就忘了自己是小球,开始像普通 AI 一样回答问题。

解法:每次调用 API 前,检查 system 消息还在不在,不在就插回去。

if self.chat_history[0]["role"] != "system":
    self.chat_history.insert(0, {
        "role": "system",
        "content": "你是一个运行在用户电脑桌面上的弹跳小球,你回答内容的长度不能超过20字"
    })

坑三:API 限流或失败怎么办?

免费 API 有调用次数限制,或者网络可能出问题。

解法:错误处理 + 本地兜底回复。

try:
    response = self.client.chat.completions.create(...)
except Exception as e:
    print(f"大模型调用失败: {e}")
    return "抱歉,我现在有点卡顿,等会儿再聊好吗?"

🤔 思考:有没有其他做法?

当然有。对话接入不止这一种方案。

方法 原理 优缺点
OpenAI SDK(本方案) 官方 Python 库调用 ✅ 简单、标准
✅ 支持流式输出
❌ 依赖网络
HTTP 直接请求 requests 调 API ✅ 更底层、可控
❌ 要自己处理认证、重试
本地模型 llama.cpp 跑本地模型 ✅ 免费、离线
❌ 吃配置、慢
预定义回复 随机返回预设句子 ✅ 极快、无网络
❌ 聊不了几句就重复

🏆 为什么选 OpenAI SDK?

因为简单。几行代码就能接入,而且模力方舟的接口和 OpenAI 兼容,直接用 SDK 省去很多麻烦。


📚 总结:从这个小球,我们又学到了什么?

🎯 一、模块化设计

把 AI 对话相关的代码单独放到 chat.py,主程序只负责调用。这叫关注点分离——每个文件只做一件事。

🔄 二、角色设定

通过 system 消息给 AI 设定“人设”,让它知道自己是“弹跳小球”,而不是万能的百科全书。

💾 三、持久化存储

用 JSON 文件保存对话历史,让小球有“记忆力”,关掉程序再打开还能记得你是谁。

🧪 四、错误处理

API 调用可能失败,要有兜底方案,给用户友好的提示而不是直接崩溃。

⚖️ 五、参数调优

temperaturemax_tokensfrequency_penalty 这些参数不是摆设,调好了能让回复更自然。


🧰 可迁移的思维工具

这套“AI 对话”的方法论,可以迁移到很多场景:

场景 要做什么 关键点
客服机器人 自动回复用户问题 角色设定、知识库
翻译工具 翻译文本 提示词工程
代码助手 生成代码 上下文管理
角色扮演 扮演特定人物 系统消息设定

💎 核心心法:无论接什么 AI,流程都是:

  1. 选择模型和 API
  2. 设计角色设定
  3. 管理对话历史
  4. 处理错误和限流
  5. 持久化存储

📝 本篇要点回顾

模块 核心内容
模型选择 模力方舟 DeepSeek-V3(每日免费 100 次)
角色设定 system 消息:“你是弹跳小球,回答 ≤20 字”
对话管理 保存历史到 JSON,下次启动时加载
参数调优 temperature=0.6, frequency_penalty=1.1
错误处理 try-catch 捕获异常,返回友好提示
模块化 单独 ChatModel 类,主程序只调用

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐