
1. 从零做一个会聊天的桌宠Tkinter 透明窗口怎么搭桌宠智能体这个东西说白了就是一只常驻桌面的小人/小动物你点它一下它会动你问它问题它会答你让它帮忙看文件它也能动手。它和普通 GUI 程序最大的区别在于窗口没有标题栏、背景透明、永远浮在桌面最上层而且不能出现在任务栏里抢焦点。这套效果用 Python 自带的 Tkinter 就能做出来不需要装 Qt 或 Electron 那种重家伙。适合谁来跟做会一点 Python 基础、想给自己桌面加个 AI 小助手、又不想被复杂框架劝退的人。整条路径分三块透明悬浮窗Tkinter、对话逻辑LLM 请求封装、模型通道管理统一 Key 和 Base URL。前两块是本地代码第三块决定你后面换模型方不方便。我先说清楚一个容易踩的坑Tkinter 的overrideredirect(True)能去掉标题栏但去掉之后窗口就不受系统窗口管理器正常管理了拖拽、关闭、置顶都得自己写。透明背景则要靠wm_attributes(-transparentcolor, ...)这个属性只在 Windows 上有效而且颜色要选一个图片里绝对不会出现的色值比如纯品红#FF00FF否则宠物边缘会被抠掉一块。窗口尺寸建议按宠物图片实际大小来别写死。加载图片用 Pillow因为它能保留 PNG 的 alpha 通道Tkinter 自带的 PhotoImage 对透明支持很差。下面这段是主窗口骨架可以直接跑import tkinter as tk from PIL import Image, ImageTk class PetWindow: def __init__(self, img_pathpet_picture/dog.png): self.root tk.Tk() self.root.overrideredirect(True) # 去掉标题栏 self.root.wm_attributes(-topmost, True) # 永远置顶 self.root.wm_attributes(-transparentcolor, #FF00FF) # 透明色键 self.root.config(bg#FF00FF) img Image.open(img_path).convert(RGBA) self.photo ImageTk.PhotoImage(img) self.label tk.Label(self.root, imageself.photo, bg#FF00FF, bd0) self.label.pack() self._bind_events() self.root.geometry(800400) # 初始位置 self.root.mainloop() def _bind_events(self): self.label.bind(Button-1, self._on_press) self.label.bind(B1-Motion, self._on_drag) def _on_press(self, e): self._dx, self._dy e.x, e.y def _on_drag(self, e): x self.root.winfo_x() e.x - self._dx y self.root.winfo_y() e.y - self._dy self.root.geometry(f{x}{y}) if __name__ __main__: PetWindow()跑起来你会看到一只没有边框的狗浮在桌面上按住能拖。这里有个细节-transparentcolor的色值必须和bg完全一致差一个十六进制位都会失效。另外如果你发现宠物周围有一圈白边多半是 PNG 本身带了白色描边不是代码问题换张干净的图就行。窗口搭好只是第一步。真正让它“活”起来的是动画和交互待机时呼吸缩放、点击时跳一下、右键弹菜单。动画不要用time.sleep在主线程里循环会把界面卡死。正确做法是用root.after(ms, callback)做定时回调每 50ms 改一次图片尺寸或窗口位置形成连续动画。呼吸动画就是让缩放系数在 0.95 到 1.05 之间来回走用正弦函数最自然import math def breathing(self, t0): scale 1.0 0.05 * math.sin(t / 10) w int(self.base_w * scale) h int(self.base_h * scale) resized self.orig_img.resize((w, h), Image.LANCZOS) self.photo ImageTk.PhotoImage(resized) self.label.config(imageself.photo) self.root.after(50, lambda: self.breathing(t 1))右键菜单用tk.Menu(tearoff0)绑定Button-3弹出。菜单项里放“大模型配置”“任务看板”“记忆管理”这些入口后面接 LLM 和工具模块都从这里进。到这一步一个能拖、能动、能弹菜单的桌宠壳子就完成了接下来才是接大脑。2. 接入 LLM 前先把 TaoToken 的 Key 和通道准备好桌宠要会聊天就得调大模型 API。这里有个现实问题市面上的模型服务商太多OpenAI、DeepSeek、通义、智谱各有各的接口地址和 Key你要是每接一个就改一次代码、存一份 Key配置会乱成一团。更麻烦的是有些服务商网络不稳定或者你想在几个模型之间切换对比效果每次都要翻代码找base_url。我的做法是走一个统一的 API 通道把 Key 和 Base URL 收敛到一处。TaoToken 就是干这个的它提供 OpenAI 兼容的接口你拿一个 Key 就能调多种模型代码里只认一个base_url换模型只改model字段。官网在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 根地址是 https://taotoken.net/api 注意这个地址后面不加任何参数。为什么强调 OpenAI 兼容因为 Python 生态里openai这个库已经成了事实标准很多第三方 SDK 都按它的格式来。你只要把base_url指过去client.chat.completions.create(...)这套调用方式原封不动就能用。桌宠的 LLM 封装层因此可以写得很薄不用为每个服务商写适配器。拿 Key 的流程不复杂进控制台创建一个 API Key复制出来存好。控制台入口是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite Key 管理页在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。Key 只在创建时完整显示一次记得当场存进密码管理器或本地配置文件别截图发群里。这里要提醒一句Key 属于敏感凭证不要硬编码进pet.py然后传到公开仓库。桌宠项目我建议单独放一个pet_config.json把 Key 写进去同时把pet_config.json加进.gitignore。运行时读取配置代码里永远不出现明文 Key。配置结构可以设计成多提供商多模型方便以后扩展。llm_providers存服务商名称、base_url、api_keyllm_models存模型属于哪个提供商、模型名、上下文窗口、压缩阈值llm_active记录当前激活的是哪个。这样你在桌宠的“大模型配置”窗口里就能可视化地增删改不用碰代码。数据库用 SQLite建表语句大致如下CREATE TABLE llm_providers ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, base_url TEXT NOT NULL, api_key TEXT NOT NULL, enabled INTEGER DEFAULT 1, sort_order INTEGER DEFAULT 0 ); CREATE TABLE llm_models ( id INTEGER PRIMARY KEY AUTOINCREMENT, provider_id INTEGER NOT NULL, name TEXT NOT NULL, enabled INTEGER DEFAULT 1, max_context_tokens INTEGER DEFAULT 32000, compress_threshold REAL DEFAULT 0.8, min_recent_rounds INTEGER DEFAULT 5 ); CREATE TABLE llm_active ( id INTEGER PRIMARY KEY, provider_id INTEGER, model_name TEXT );填配置的时候base_url填https://taotoken.net/apiapi_key填你刚创建的那串model填你想用的模型 ID。具体有哪些模型 ID 可以看接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。文档里会列当前支持的模型名照着填就行。如果你后面打算长期跑编码类或 Agent 类任务比如让桌宠帮你写代码、调工具、跑多步流程可以考虑 Coding Plan入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。它和按量计费的 Key 是两套东西按自己的使用频率选。想先单纯试试模型对话效果可以用模型对话页https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 。把 Key 和通道准备好之后桌宠的 LLM 封装层就只剩一件事发请求、收流式响应、把文字塞进气泡或对话窗口。下一节直接上可复制的代码。3. 可复制的 LLM 请求封装与桌宠配置片段这一节给你能直接抄进项目的代码。先说配置文件再说请求封装最后说怎么和 Tkinter 的界面线程配合。配置文件pet_config.json长这样路径白名单和 Shell 白名单也一并放进来后面工具调用要用{ image: pet_picture/dog.png, llm: { base_url: https://taotoken.net/api, api_key: sk-你的Key填这里, model: 你的模型ID, max_context_tokens: 32000, compress_threshold: 0.8, min_recent_rounds: 5 }, allowed_paths: [ C:/Users/你的用户名/Desktop, C:/Users/你的用户名/Documents, C:/Users/你的用户名/Downloads ], enabled_shell_commands: [ipconfig, ping, dir, tree, systeminfo], custom_shell_commands: [] }注意base_url写https://taotoken.net/api不要在后面加/v1或斜杠OpenAI SDK 会自己拼路径。api_key和model换成你自己的。这个文件放在项目根目录和main.py同级。请求封装用openai库先pip install openai。封装成一个类支持流式输出因为桌宠对话要逐字显示才有感觉import json from openai import OpenAI class LLMClient: def __init__(self, config_pathpet_config.json): with open(config_path, r, encodingutf-8) as f: cfg json.load(f)[llm] self.model cfg[model] self.client OpenAI( base_urlcfg[base_url], api_keycfg[api_key], ) def chat_stream(self, messages, on_deltaNone): 流式对话每收到一段文字就回调 on_delta resp self.client.chat.completions.create( modelself.model, messagesmessages, streamTrue, ) full for chunk in resp: delta chunk.choices[0].delta if delta and delta.content: full delta.content if on_delta: on_delta(delta.content) return full def chat(self, messages): 非流式用于生成题目、摘要等短任务 resp self.client.chat.completions.create( modelself.model, messagesmessages, ) return resp.choices[0].message.contentmessages是标准格式[{role: system, content: ...}, {role: user, content: ...}]。多轮对话就是把历史消息按顺序拼进去。桌宠的系统提示词可以写成“你是一只桌面宠物说话简短可爱回答控制在三句话内”这样气泡不会撑爆。关键点来了Tkinter 是单线程的网络请求会阻塞界面。如果你在按钮回调里直接调chat_stream整个窗口会卡住直到响应结束宠物动画全停。解决办法是把请求丢到后台线程用root.after把结果安全地送回主线程更新 UIimport threading def on_send(self, user_text): self.messages.append({role: user, content: user_text}) self.append_bubble(你, user_text) def worker(): def on_delta(text): # 不能直接改 UI用 after 排队 self.root.after(0, lambda: self.append_stream(text)) reply self.llm.chat_stream(self.messages, on_delta) self.messages.append({role: assistant, content: reply}) threading.Thread(targetworker, daemonTrue).start()daemonTrue保证主窗口关闭时线程不会拖着进程不放。root.after(0, ...)是 Tkinter 里跨线程更新 UI 的标准姿势比直接操作控件安全得多。上下文压缩也在这里做。每次发请求前估算 token 数超过max_context_tokens * compress_threshold就把中间的历史消息用 LLM 摘要成一段 100-200 字的中文替换掉原始消息只保留最近min_recent_rounds轮完整对话。这样长对话不会爆窗口也不会丢关键信息。如果你用的是 Claude Code 这类工具做辅助开发配置方式类似Base URL 填https://taotoken.net/apiKey 和 Model ID 按文档填。Claude Code 的接入说明在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里能找到。三件套永远是Base URL、Key、Model ID缺一不可。配置和封装都齐了下一节验证请求能不能通。4. 验证请求与本地运行从 curl 到桌宠气泡写完代码别急着接界面先用最小请求验证通道是通的。这一步能帮你把“代码问题”和“配置问题”分开省很多排查时间。最直接的方式是用 curl 打一发curl https://taotoken.net/api/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer sk-你的Key \ -d { model: 你的模型ID, messages: [{role: user, content: 用一句话介绍你自己}], stream: false }如果返回 JSON 里有choices[0].message.content说明 Key、Base URL、模型 ID 三样都对。如果报 401是 Key 问题报 404多半是模型 ID 写错或 Base URL 多了路径报连接超时检查网络和地址拼写。curl 通了之后跑 Python 版from llm.client import LLMClient client LLMClient(pet_config.json) reply client.chat([ {role: system, content: 你是一只桌面宠物说话简短。}, {role: user, content: 今天适合做什么}, ]) print(reply)能打印出中文回复就说明封装层没问题。这时候再把它接到桌宠的气泡上。气泡窗口本身也是个 Toplevel无边框、半透明、自动消失class Bubble: def __init__(self, root, text, x, y, duration3000): self.win tk.Toplevel(root) self.win.overrideredirect(True) self.win.wm_attributes(-topmost, True) self.win.wm_attributes(-alpha, 0.9) self.win.geometry(f{x}{y}) tk.Label(self.win, texttext, bg#FFF8DC, font(微软雅黑, 10), wraplength220, justifyleft, padx10, pady8).pack() self.win.after(duration, self.win.destroy)流式输出时每收到一段 delta 就更新气泡里的文字。因为气泡是动态创建的简单做法是第一次收到 delta 时创建气泡后续 delta 往同一个 Label 追加。注意wraplength要设不然长回复会横向拉成一条线。实测下来从点击发送到第一个字出现延迟主要在网络往返通常几百毫秒到一两秒。如果明显卡顿先确认是不是在主线程里发了请求。另一个常见现象是流式输出时气泡闪烁那是每次追加都重建了 Label改成label.config(text...)复用同一个控件就好。验证通过后你可以让桌宠做点实际的事比如右键菜单点“每日名言”后台调chat生成一句显示在气泡里点“百科问答”打开对话窗口走多轮chat_stream。这些功能共用同一个LLMClient实例不要每次新建否则连接池反复重建效率低。到这一步一个能聊天、能弹气泡、能流式输出的桌宠就跑起来了。接下来是排错环节这些报错我基本都遇到过。5. 常见报错排查401、local proxy failed、reading choices、OAuth桌宠接 LLM 最容易卡在几个固定报错上我按出现频率排一下对照着查。401 Unauthorized。这是 Key 的问题不是代码问题。检查三处pet_config.json里的api_key有没有多余空格或换行Key 是不是已经删除或过期请求头里Authorization: Bearer后面有没有漏空格。用 curl 单独测一次能排除代码干扰。如果 curl 也 401去控制台重新创建一个 Keyhttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。local proxy failed / connection error。这个报错通常出现在base_url写错或网络环境异常时。先确认地址是https://taotoken.net/api没有多余路径、没有尾部斜杠。如果你本地配了系统级网络工具可能会拦截请求临时关掉再试。还有一种情况是公司网络对某些域名有限制换个网络环境验证一下。这个报错和代码无关别去改openai库的源码。Error reading choices / list index out of range。这个报错说明响应体里没有choices字段或者choices是空数组。常见原因有三个一是模型 ID 写错服务端返回了错误 JSON但代码直接去取choices[0]就崩了二是流式模式下某些 chunk 的delta为空你没做判空三是请求被限流返回了错误结构。解决办法是在解析前先判空for chunk in resp: if not chunk.choices: continue delta chunk.choices[0].delta if delta and delta.content: ...非流式同理取resp.choices[0].message.content前先确认resp.choices非空。这个判空能挡掉一大半诡异崩溃。OAuth / authentication 相关报错。如果你用的是 Claude Code 或某些 CLI 工具它们可能默认走 OAuth 登录流程而不是 API Key。这时候要在配置里显式指定用 API Key 模式把 Base URL 和 Key 填进对应的配置文件。Claude Code 的配置方式看文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。核心还是三件套Base URL、Key、Model ID三个都对就不会报认证错。流式输出卡住不结束。检查是不是在for chunk in resp循环里做了耗时操作比如每收一个字就重建整个对话窗口。正确做法是累积文本定时刷新 UI。另外确认streamTrue时没有在循环外提前return。中文乱码。Windows 下 Tkinter 默认字体对中文支持还行但如果气泡里出现方块把字体显式设成(微软雅黑, 10)。文件读写统一用encodingutf-8SQLite 存中文没问题但json.dumps时加ensure_asciiFalse才不会变成\uXXXX。工具调用不触发。如果你接了工具调用function calling模型不调工具通常是提示词没写清楚或者工具描述太模糊。把每个工具的名称、参数、用途写具体系统提示词里明确说“需要读写文件时调用对应工具”。另外确认你用的模型支持 function calling不是所有模型都支持。这些坑踩完桌宠基本就稳了。最后说下怎么把它用起来。6. 把桌宠用起来从对话到工具调用的下一步代码跑通、报错排完接下来是让它真正帮你干活。桌宠的价值不在于“有个宠物”而在于它把 LLM 能力塞进了你每天都会看到的桌面角落随手就能用。最基础的用法是对话。右键点宠物打开对话窗口问它问题。多轮上下文靠messages列表维护每轮把用户和助手消息都追加进去。历史会话存 SQLiteagent_sessions存会话元信息agent_messages存每条消息切换会话时按session_id查出来重新渲染。消息多了要懒加载一次只渲染最近 5 条往上滚再加载更早的不然窗口会卡。进阶用法是工具调用。给模型注册几个工具读文件、写文件、列目录、执行白名单命令、打开文件。模型判断需要时返回tool_calls你在代码里执行对应函数把结果作为role: tool的消息再发回去模型据此生成最终回复。安全上做三层控制路径白名单只允许访问桌面、文档、下载和项目目录Shell 命令白名单只放行ipconfig、ping、dir这类只读命令未授权操作弹窗让用户确认。这样即使模型判断失误也伤不到系统。再进一步是记忆。对话结束后让模型检查这轮有没有值得记住的偏好比如“用户喜欢喝美式”“用户在做 Python 项目”存进personal_memories表字段包括 key、value、topic、keywords、importance。下次新建对话时把 importance 大于等于 3 的记忆拼进系统提示词宠物就“记得”你了。记忆检索用关键词 LIKE 加向量语义搜索双轨关键词命中快向量补语义效果比单一路径好。如果你想让桌宠调用外部工具或 MCP 服务器配置入口在文档里https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。MCP 本质是标准化的工具协议桌宠作为客户端连上去就能用别人写好的工具。注意别把 MCP 直连到生产数据库测试环境先跑通再说。长期跑编码或 Agent 任务的话Coding Plan 比按量计费更划算入口https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。只是偶尔聊聊天、生成点小内容用普通 Key 就够了。最后给个实用建议桌宠项目别一上来就堆功能。先把透明窗口、拖拽、气泡、单轮对话跑通这四样是骨架。骨架稳了再往上加任务看板、答题、记忆、工具调用。我见过太多人卡在“想做的功能太多结果一个都没做完”。先让它能聊天再让它能干活最后让它记得你。