搭建自己的AI API对话机器人UI程序完全指南(有完整代码,在Python3.13环境下即拿即用)
在人工智能大模型时代,越来越多的开发者和企业需要集成先进的AI模型到自己的应用中。然而,直接调用各个模型的官方API存在诸多痛点:需要分别注册多个平台账户、维护不同的API密钥、处理各异的接口规范、承担高昂的计费成本。为了解决这些问题,API聚合平台应运而生。本项目基于API镜像站这一高效的OpenAI接口聚合管理平台,融合了丰富的免费和付费模型资源,为用户提供了一套完整的、开箱即用的AI对话机器
目录
本文详细介绍如何将API独立站上的模型嵌入UI程序,打造自己的聊天机器人。API独立站点我就能打开。
第一章 项目概述与核心特性
1.1 项目背景与意义
在人工智能大模型时代,越来越多的开发者和企业需要集成先进的AI模型到自己的应用中。然而,直接调用各个模型的官方API存在诸多痛点:需要分别注册多个平台账户、维护不同的API密钥、处理各异的接口规范、承担高昂的计费成本。为了解决这些问题,API聚合平台应运而生。本项目基于API镜像站这一高效的OpenAI接口聚合管理平台,融合了丰富的免费和付费模型资源,为用户提供了一套完整的、开箱即用的AI对话机器人解决方案。这套解决方案采用Python与Tkinter框架开发,打造了一个功能强大、界面友好的桌面应用程序,使得普通用户无需复杂的技术背景,就能轻松体验各种最先进的AI模型。
1.2 核心功能特性
这个AI对话机器人应用集成了众多实用功能,使其成为一个专业级的AI交互工具。首先,应用支持实时流式响应,用户输入的问题能够以流式形式返回AI的回答,提供了更加自然流畅的交互体验。其次,应用内置了完整的Markdown格式支持,可以正确渲染标题、加粗、斜体、代码块等各种排版元素,让AI的回答更加易读。第三,应用特别设计了代码提取功能,当AI在回答中包含代码段时,系统会自动识别并提取这些代码块到独立的选项卡中,方便用户查看和复制。此外,应用还提供了丰富的参数调整选项,包括温度、Top P等采样参数,允许用户根据不同场景灵活调整模型的生成行为。应用还支持完整的对话导出功能,可以将整个对话历史保存为文本文件以供日后查阅,同时支持从JSON文件导入设置,方便用户备份和恢复自己的配置。
第二章 环境与依赖准备
2.1 系统需求与Python环境
为了成功运行这个AI对话机器人应用,用户需要准备一个合适的开发环境。应用要求Python 3.7或以上版本,因为代码中使用了f-string等现代Python特性。在操作系统方面,由于应用使用了Tkinter作为GUI框架,理论上可以运行在Windows、macOS和Linux等多个平台上,但在实际使用中,Windows和macOS用户体验通常最佳。建议用户在安装Python时,确保勾选了"Add Python to PATH"选项,这样可以在命令行中直接调用Python。
2.2 必需的Python库安装
项目的核心依赖包括requests库用于发送HTTP请求、json库用于处理JSON数据、threading库用于多线程操作。这些库中,requests是唯一需要额外安装的第三方库,而json和threading已经包含在Python标准库中。用户可以通过以下命令安装所有必需的库:
pip install requests
对于需要使用最新版本或特定版本的requests库的用户,可以使用以下命令:
pip install --upgrade requests
如果用户所在的网络环境访问PyPI速度较慢,可以考虑使用国内镜像源,例如阿里云镜像或清华大学镜像。一个完整的带有镜像源的安装命令示例为:
pip install -i https://mirrors.aliyun.com/pypi/simple/ requests
2.3 API服务账户注册与配置
在开始使用本应用之前,用户必须在api.aigc.bar平台上注册账户并获取API密钥。注册过程非常直观:用户需要访问这个注册链接,按照页面提示填写基本信息如邮箱地址和密码,完成邮箱验证后即可注册成功。

注册完成后,用户进入令牌管理页面,根据实际需求添加令牌,将看到自己生成的API密钥。这个密钥是访问所有AI模型的凭证,用户需要妥善保管并在应用启动时输入。为了安全起见,不建议在代码中硬编码API密钥,而应该通过环境变量或配置文件的方式存储。本应用设计了一个自动保存机制,用户首次输入API密钥后,点击"保存"按钮,系统会将密钥保存到本地文件中,下次启动应用时会自动加载,无需重复输入。
如使用免费的,则使用这个:

如果使用高级点的模型,也可充值后使用付费分组。
第三章 应用架构与核心代码解析
3.1 整体架构设计与类结构
这个应用的设计采用了面向对象编程范式,由两个主要类组成:MarkdownFormatter和ChatBotUI。MarkdownFormatter类专门负责处理Markdown格式的文本转换和渲染,它包含了两个重要的静态方法,一个是extract_code_blocks方法,用于从AI返回的文本中识别并提取代码块,另一个是format_text方法,用于将Markdown格式的文本正确地插入到Tkinter的Text widget中,并应用相应的标签样式。ChatBotUI类是整个应用的主类,负责创建用户界面、处理用户交互、管理对话历史、与API通信等核心业务逻辑。这种分离的设计使得代码结构清晰,各个模块职责明确,易于维护和扩展。
3.2 Markdown处理引擎
Markdown处理引擎是这个应用的一个独特之处,它能够正确地识别和渲染各种Markdown格式元素。extract_code_blocks方法使用了正则表达式来识别代码块,具体使用的模式是r'```(?:python|py)?\s*\n(.*?)\n```',这个模式能够匹配三个反引号包围的代码块,并且支持可选的python语言标识符。该方法的核心逻辑是遍历所有匹配到的代码块,逐个提取其内容,同时删除原文本中的代码块标记,返回处理后的文本和代码块列表。这样做的好处是,AI返回的代码会被自动分离出来,可以在独立的选项卡中以高亮的代码编辑器样式显示,提升了代码的可读性。
format_text方法的工作原理更加复杂,它逐行处理文本,首先检查每一行是否是标题(以#开头),如果是标题则根据#的数量确定标题级别(1到3级),应用相应的标签样式。对于非标题行,方法会逐个字符地处理,在每个位置使用正则表达式同时检查多种格式标记:文本(加粗)、文本(斜体)、文本(代码)和链接文本(链接)。当发现格式标记时,取出最先出现的那个,应用相应的tag标签,然后继续处理剩余的文本。这种逐个处理的方式虽然计算量较大,但保证了格式的正确性,避免了格式标记之间的冲突。
3.3 UI界面构建与布局设计
应用的用户界面采用了左右分栏的经典布局设计。左侧是一个统一的参数配置面板,使用了LabelFrame容器来组织各个参数控制元素。这个面板包含了API密钥管理、模型选择、系统提示词、生成参数调整等各个功能区域。API密钥部分采用了一个Frame容器,在其中水平排列了一个Entry输入框、一个"保存"按钮和一个"注册"按钮,使得用户可以方便地输入密钥或快速跳转到注册页面。系统提示词使用了ScrolledText组件,这样即使提示词内容过长也能正常显示。参数调整部分(最大输出长度、温度、Top P)采用了Spinbox和Scale等专门的组件,提供了图形化的参数调整体验。
右侧是主要的聊天和代码展示区域,采用了Notebook(选项卡)组件来实现多选项卡的界面。默认情况下,应用启动时会创建一个"聊天"选项卡,显示聊天内容。当AI返回的内容中包含代码块时,应用会动态地添加新的代码选项卡,每个代码块对应一个独立的选项卡。这样的设计使得用户可以轻松地在聊天内容和代码之间切换查看,而无需在冗长的对话历史中来回翻滚。

3.4 核心通信机制
应用与API的通信采用了一个精心设计的流程。当用户点击"发送"按钮或按下Ctrl+Enter快捷键后,应用首先会验证API密钥是否已输入,然后检查用户是否正在等待上一条消息的回复。通过验证后,用户的消息会被添加到对话历史中,UI中会显示用户消息,输入框会被清空。接着,应用会设置一个标志位is_loading为True,禁用发送按钮,更新状态标签为"正在处理...",并在后台开启一个新的工作线程来处理API请求,避免阻塞主UI线程。
在工作线程中,应用构造了一个遵循OpenAI API格式的请求payload,包含了用户选择的模型、完整的消息历史(包括系统提示词)、各种采样参数。该payload被发送到api.aigc.bar的v1/chat/completions端点。应用使用了stream=True参数来启用流式响应,这意味着API会立即开始返回结果,而不是等待完整的响应生成。应用通过response.iter_lines()方法逐行迭代处理返回的数据,每一行都是一个符合Server-Sent Events格式的JSON对象。对于包含reasoning_content字段的数据块,应用将其识别为模型的思考过程(某些模型如DeepSeek推理模型会返回此字段),并以特殊的"thinking"样式显示。对于包含content字段的数据块,应用将其作为最终回答,以"assistant"样式显示。
第四章 免费模型与基础使用
4.1 可用的免费模型列表
api.aigc.bar平台为用户提供了多个免费使用的AI模型,这些模型涵盖了多个领域和能力水平。目前应用代码中预配置的免费模型包括以下几个:首先是GPT系列模型中的gpt-4.1-nano和gpt-5-nano这两个轻量级版本,这些模型虽然参数量较小,但在基础的文本生成和问题回答方面表现可靠,适合那些对响应速度有要求或输入量较小的场景。其次是DeepSeek系列的deepseek-r1-0528和deepseek-v3两个模型,其中r1模型是推理专向模型,特别擅长数学、编程等需要逻辑推理的任务,会返回详细的思考过程;deepseek-v3是多用途模型,在知识问答、文本生成等方面表现均衡。此外还有deepseek-v3.1,这是v3的一个改进版本。最后是gemini-2.5-flash-lite,这是Google最新推出的轻量化Gemini模型,在多模态理解和快速响应方面表现优异。这些免费模型的可用额度通常有限制,新用户一般会获得初始的免费试用额度,具体的额度和有效期需要用户在平台上查看。

4.2 基础使用流程与最佳实践
使用本应用的基础流程非常简洁。首先,用户启动应用后,在左侧参数面板的"API密钥"输入框中粘贴自己的API密钥,然后点击"保存"按钮,系统会将密钥保存到本地。其次,用户在"选择模型"下拉框中选择想要使用的模型,比如选择gpt-4.1-nano进行快速体验。如果需要自定义AI的行为风格,可以在"系统提示词"的大文本框中修改系统提示,比如可以将默认的中文助手改为特定行业的专家角色。然后用户在右侧的输入框中输入想要问的问题,点击"发送"按钮或按下Ctrl+Enter快捷键,应用就会向API发送请求。等待几秒钟后(具体时间取决于模型复杂度和网络延迟),AI的回答就会以流式的形式出现在上方的聊天展示区,用户可以实时看到回答的逐字生成过程。如果AI的回答中包含代码块,应用会自动创建新的代码选项卡,用户可以点击选项卡查看代码,右键选择"复制代码"将代码复制到剪贴板。
在实际使用中,为了获得更好的效果,用户应该注意以下几个最佳实践:首先,在提问时要尽可能清晰具体,提供充足的上下文信息,这样AI能够更准确地理解需求。其次,对于需要长篇幅回答或深度思考的问题,应该选择较高的max_tokens值(比如8192或更高),同时可以适当提高温度参数以获得更创意的回答。第三,对于对答案准确性有严格要求的场景(比如代码生成、数学计算),应该选择推理能力强的模型(如deepseek-r1)或设置较低的温度值(0.3-0.5)以获得更一致的回答。第四,如果需要维持一个连贯的多轮对话,应该避免频繁地清空对话历史,这样AI可以基于之前的上下文进行更一致的回答。
第五章 付费模型配置与进阶使用
5.1 付费模型的种类与定价体系
虽然API平台提供了多个免费模型供初级用户体验,但对于需要更强大能力的用户,平台还提供了多个付费模型选项。付费模型通常分为几个层级:基础层模型包括像gpt-5-mini这样的快速、经济的模型,适合日常文本生成和简单问答任务,价格最低廉;中端层模型包括gpt-4.1、claude-code等,这些模型具有更强的理解能力和生成质量,适合需要更高准确度的任务;高端层模型包括gpt-5.1、claude-4.5-opus、claude-4.5-sonnet等最新发布的模型,具有最强的能力,适合复杂的推理、创意写作等高端任务;专用模型则包括gemini-pro(Google的旗舰模型)、llama-2等开源模型的托管版本,这些模型在特定领域可能有独特的优势。平台通常采用按照输入token数和输出token数分别计费的模式,不同模型的价格差异较大,用户需要根据自己的预算和需求选择合适的模型。

5.2 修改代码以使用付费模型
要在应用中使用付费模型,用户需要修改ChatBotUI类中的models列表。原始的模型列表定义在__init__方法中,看起来如下:
self.models = [
"gpt-4.1-nano",
"gpt-5-nano",
"deepseek-r1-0528",
"deepseek-v3",
"deepseek-v3.1",
"gemini-2.5-flash-lite"
]
要添加付费模型,用户只需要在这个列表中添加新的模型字符串。例如,如果用户想添加OpenAI的GPT-4 Turbo模型和Claude 3.5 Sonnet模型,修改后的列表应该如下所示:
self.models = [
"gpt-4.1-nano",
"gpt-5-nano",
"deepseek-r1-0528",
"deepseek-v3",
"deepseek-v3.1",
"gemini-2.5-flash-lite",
"gpt-4-turbo",
"gpt-4-turbo-preview",
"claude-3.5-sonnet",
"claude-3-opus",
"gemini-pro"
]
添加完模型后,当用户启动应用时,在"选择模型"下拉框中就会看到新增的模型选项。用户可以选择任何一个付费模型,然后像使用免费模型一样发送消息,应用会自动调用该模型的API。需要注意的是,一旦选择了付费模型,每一次API调用都会根据平台的定价产生费用。为了避免意外的大额消费,建议用户在实验新模型时先使用较小的max_tokens值,并且在平台上设置每日或每月的消费上限。
5.3 不同分组下的模型选择指南
在api.aigc.bar平台上,各个模型通常按照功能分组进行组织。第一个分组是通用文本生成模型组,这一组包括所有的GPT系列模型和通用的Claude模型,这些模型在各种任务上都有不错的表现,适合那些没有特殊需求的一般用户。用户如果不确定选择哪个模型,可以考虑从这个分组中选择。第二个分组是推理和编程专向模型组,这一组主要包括DeepSeek的r1和v3系列,以及OpenAI的GPT-4系列,这些模型在代码生成、数学推理、逻辑问题解决等方面表现特别突出,适合开发人员和科研工作者。如果用户的任务涉及编写复杂代码或解决深层次的逻辑问题,应该优先选择这个分组中的模型。第三个分组是多模态模型组,包括Google的Gemini系列和其他支持图像输入的模型,虽然本应用的当前版本主要处理文本,但这些模型的文本生成能力也很强,在需要更全面理解的场景下表现更好。
对于不同场景,用户有以下建议的选择策略:如果进行日常问答和文本生成,推荐使用gpt-4.1-nano或gpt-5-nano这样的免费轻量级模型,既能保证质量又能节省成本。如果需要编写代码或解决数学问题,推荐使用deepseek-r1-0528或deepseek-v3,因为这两个模型在代码质量和推理准确性上都很突出,而且deepseek-r1-0528会返回详细的思考过程,帮助用户理解AI的推理逻辑。如果追求最好的输出质量且预算充足,可以选择付费的gpt-5.2或claude-4.5-sonnet,这两个模型在创意写作、深度分析等复杂任务上能力最强。如果进行快速原型设计或对延迟有严格要求,可以选择gemini-2.5-flash-lite或其他标注为"flash"的轻量版本,这些模型的响应速度最快。
第六章 高级功能与参数优化
6.1 采样参数的作用与调整方法
应用中提供的温度(Temperature)和Top P两个采样参数是控制模型生成行为的关键因素。温度参数的取值范围是0到2,它控制了模型生成结果的随机性程度。当温度设置为0时,模型会采用最贪心的策略,总是选择概率最高的下一个token,生成的结果最确定、最可预测,适合对答案准确性要求很高的场景如代码生成或数据提取。当温度设置为1时,这是一个平衡点,模型会以正常的概率分布进行采样。当温度设置为更高的值(如1.5或2)时,模型会倾向于选择概率较低但更多样化的token,生成的结果会更富创意、更多样化,适合创意写作或头脑风暴场景。Top P参数的取值范围是0到1,它采用核采样的策略,表示只从累计概率达到P的最可能的token中进行采样。例如,当Top P设置为0.9时,模型只会在那些累计概率达到90%的token中进行选择,这样可以避免选择极低概率的离奇token,同时保留一定的多样性。
在实际使用中,一个常用的参数组合是温度0.7、Top P 0.9,这个组合在多数场景下提供了很好的平衡。当用户发现模型的回答显得过于保守或重复时,可以尝试提高温度值或降低Top P值。相反,当模型的回答显得过于随意或与问题偏离时,应该降低温度或提高Top P值。
6.2 系统提示词的设计与优化
系统提示词(System Prompt)是指导AI行为的关键指令。应用中默认的系统提示词是"你是一个专业、友善且富有创意的AI助手。你会用中文回答用户的问题。"这个提示词设置了基本的角色定义和语言偏好。但用户可以根据实际需求定制系统提示词,来引导AI产生特定风格或领域的回答。例如,如果用户想让AI扮演一个C++编程专家的角色,可以将系统提示词改为"你是一位资深的C++工程师,拥有20年的软件开发经验。当用户提出关于C++的问题时,你应该从实战经验出发,提供深入、专业的解答,并给出最佳实践建议。"又或者,如果用户想让AI充当英文翻译助手,可以设置"你是一位资深的翻译工作者,精通英文和中文。你的任务是准确、优雅地将用户提供的文本翻译成指定的语言,保留原文的含义和风格。"这样的系统提示词。设计好的系统提示词应该包括以下几个要素:清晰的角色定义(你是什么角色)、明确的任务目标(你要做什么)、输出格式要求(如果需要)、以及任何特殊的限制或偏好。
6.3 对话历史管理与导出
应用实现了完整的对话历史管理功能。在后台,应用使用一个列表来存储所有的对话消息,每条消息包括"role"(消息发送者的角色,可以是"user"或"assistant")和"content"(消息的实际内容)两个字段。这个历史列表在内存中维护,用户与AI的每一条往来都会被记录。当用户清空对话时,这个列表会被重置。应用还提供了"导出对话"功能,用户点击这个按钮后,应用会打开一个文件保存对话框,用户可以选择保存位置和文件名。导出的文件是纯文本格式,包含了对话时间戳、使用的模型名称以及完整的对话内容,这样用户可以保存重要的对话记录供日后参考,或者用于分享讨论或学习。
应用还支持"导入设置"功能,这允许用户从一个JSON文件中加载之前保存的配置。用户可以手动编辑这样的JSON文件来快速设置多个配置方案。一个典型的设置JSON文件看起来如下:
{
"api_key": "sk-xxxxxxxxxxxx",
"model": "deepseek-v3",
"system_prompt": "你是一位资深的Python编程专家",
"max_tokens": 8192,
"temperature": 0.7,
"top_p": 0.9
}
用户可以为不同的使用场景创建多个这样的配置文件,然后根据需要导入不同的配置,这样可以大大提高工作效率。
第七章 故障排查与优化建议
7.1 常见问题与解决方案
在使用过程中,用户可能会遇到各种问题。首先是连接错误,如果应用显示"连接失败",通常意味着无法连接到API站的服务器。这可能是由于网络问题、DNS解析失败或API服务器暂时不可用导致的。解决方法是检查网络连接是否正常(可以尝试用浏览器打开这个API独立站确认),确认API地址是否正确,如果还是无法连接可以尝试更换网络或使用VPN。其次是超时错误,应用的默认超时时间是120秒,如果在这个时间内没有收到完整的响应,就会显示超时错误。这通常发生在max_tokens设置过高、网络延迟大或API服务器响应慢的情况下。解决方法是降低max_tokens值,或者选择一个更轻量化的模型,或者等待一段时间后重试。第三个常见问题是API错误,应用会显示具体的错误码和错误信息。最常见的是401错误(未授权),这表示API密钥无效或已过期,需要检查API密钥是否正确输入。还有429错误(请求过于频繁),这表示API请求频率超过了平台的限制,需要降低请求频率。
7.2 性能优化与最佳实践
为了获得最佳的使用体验,用户应该注意以下几个优化点。首先,合理设置max_tokens参数,不要盲目设置为最大值。对于一般的问答任务,2048或4096的max_tokens通常就足够了,只有在需要生成长篇幅内容时才需要设置更高的值。较低的max_tokens不仅会提高响应速度,还会降低API调用成本。其次,在切换模型时要考虑速度和质量的权衡。轻量级模型(nano版本、flash版本)响应速度快但生成质量可能较低,适合快速原型和实时交互。复杂模型(turbo版本、opus版本)生成质量高但响应较慢,适合对质量有要求的任务。第三,利用系统提示词来约束模型的行为,一个好的系统提示词可以大大提升回答的相关性和准确性,减少冗余内容,从而提高token利用效率和成本效益。第四,定期检查api.aigc.bar平台上的消费统计和余额,设置合理的日均或月均消费上限,避免因为过度使用导致意外的费用。
第八章 总结与展望
本文详细介绍了如何基于api.aigc.bar平台和Python Tkinter框架,搭建一个功能完整、易于使用的AI对话机器人应用。从环境准备、基础使用、到高级功能,我们覆盖了使用者可能需要了解的各个方面。通过这个应用,用户可以轻松地体验各种免费和付费的先进AI模型,包括OpenAI的GPT系列、DeepSeek的推理模型、Google的Gemini模型等。应用内置的Markdown渲染、代码提取、流式响应等特性,使得与AI的交互更加高效和愉快。
展望未来,这个应用还有很多潜在的扩展方向。例如,可以添加多文件上传和处理能力,让用户可以直接在应用中分析文档、图片等多种文件格式。可以集成本地模型支持,让用户选择在本地运行轻量级模型而不仅仅依赖云端API。可以添加插件系统,让第三方开发者为应用开发各种功能扩展。可以实现更智能的对话管理,比如自动摘要、主题提取等功能。随着AI技术的不断进步,API站这样的聚合平台会集成越来越多的新模型,而这个应用框架已经为这样的扩展做好了准备。希望通过本文,开发者和用户能够充分利用这个强大的工具,探索AI的无限可能性。
窗体程序完整代码实现如下:
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox, filedialog
import requests
import json
import threading
from datetime import datetime
import os
import re
import webbrowser
class MarkdownFormatter:
"""处理Markdown格式化"""
@staticmethod
def extract_code_blocks(text):
"""提取代码块,返回 (非代码文本, 代码块列表)"""
code_blocks = []
# 更灵活的正则表达式,支持多种格式
pattern = r'```(?:python|py)?\s*\n(.*?)\n```'
matches = list(re.finditer(pattern, text, flags=re.DOTALL))
# 提取所有代码块
for match in matches:
code_content = match.group(1).strip()
if code_content:
code_blocks.append(code_content)
# 替换代码块为空字符串,保留其他文本
text_without_code = re.sub(pattern, '', text, flags=re.DOTALL)
return text_without_code.strip(), code_blocks
@staticmethod
def format_text(widget, text, base_tag=""):
"""将Markdown格式的文本插入到Text widget"""
widget.config(state=tk.NORMAL)
# 分行处理
lines = text.split('\n')
for line_idx, line in enumerate(lines):
# 处理标题 # ## ###
heading_match = re.match(r'^(#{1,6})\s+(.+)$', line)
if heading_match:
level = min(len(heading_match.group(1)), 3)
content = heading_match.group(2)
widget.insert(tk.END, content, f"heading{level}")
if line_idx < len(lines) - 1:
widget.insert(tk.END, '\n')
continue
# 处理该行中的Markdown格式
pos = 0
while pos < len(line):
# 查找所有格式
bold_match = re.search(r'\*\*(.+?)\*\*', line[pos:])
italic_match = re.search(r'(?<!\*)\*([^*]+?)\*(?!\*)', line[pos:])
code_match = re.search(r'`([^`]+?)`', line[pos:])
link_match = re.search(r'\[([^\]]+)\]\(([^\)]+)\)', line[pos:])
matches = []
if bold_match:
matches.append(('bold', bold_match.start(), bold_match.end(), bold_match.group(1), '**'))
if italic_match:
matches.append(('italic', italic_match.start(), italic_match.end(), italic_match.group(1), '*'))
if code_match:
matches.append(('code', code_match.start(), code_match.end(), code_match.group(1), '`'))
if link_match:
matches.append(('link', link_match.start(), link_match.end(), link_match.group(1), 'link'))
if not matches:
# 没有格式,直接插入剩余文本
widget.insert(tk.END, line[pos:], base_tag)
break
# 找到最早出现的匹配
matches.sort(key=lambda x: x[1])
match_type, start, end, content, marker = matches[0]
# 插入匹配前的文本
if start > 0:
widget.insert(tk.END, line[pos:pos + start], base_tag)
# 插入格式化的文本
if match_type == 'link':
widget.insert(tk.END, content, 'link')
else:
widget.insert(tk.END, content, match_type)
pos += end
# 添加换行符(除了最后一行)
if line_idx < len(lines) - 1:
widget.insert(tk.END, '\n')
widget.config(state=tk.DISABLED)
class ChatBotUI:
def __init__(self, root):
self.root = root
self.root.title("AI聊天机器人")
self.root.geometry("1600x800")
self.api_key = ""
self.api_url = "https://api.aigc.bar/v1/chat/completions"
self.register_url = "https://api.aigc.bar/register?aff=UP4F"
self.tutorial_url = "https://blog.csdn.net/nmdbbzcl/article/details/156234830"
self.api_key_file = os.path.join(os.getcwd(), "api_key.txt")
self.models = [
"gpt-4.1-nano",
"gpt-5-nano",
"deepseek-r1-0528",
"deepseek-v3",
"deepseek-v3.1",
"gemini-2.5-flash-lite"
]
self.conversation_history = []
self.is_loading = False
self.code_tab_count = 0
self.setup_fonts()
self.load_api_key()
self.create_ui()
def setup_fonts(self):
"""设置中文字体"""
try:
self.chinese_font = ("微软雅黑", 10)
self.title_font = ("微软雅黑", 11, "bold")
self.large_font = ("微软雅黑", 12, "bold")
self.code_font = ("Courier New", 10)
except:
try:
self.chinese_font = ("WenQuanYi Micro Hei", 10)
self.title_font = ("WenQuanYi Micro Hei", 11, "bold")
self.large_font = ("WenQuanYi Micro Hei", 12, "bold")
self.code_font = ("Courier New", 10)
except:
self.chinese_font = ("Helvetica", 10)
self.title_font = ("Helvetica", 11, "bold")
self.large_font = ("Helvetica", 12, "bold")
self.code_font = ("Courier New", 10)
def load_api_key(self):
"""从文件加载API密钥"""
try:
if os.path.exists(self.api_key_file):
with open(self.api_key_file, 'r', encoding='utf-8') as f:
api_key = f.read().strip()
if api_key:
self.api_key = api_key
except Exception as e:
print(f"加载API密钥失败: {e}")
def save_api_key(self, api_key):
"""保存API密钥到文件"""
try:
if api_key:
with open(self.api_key_file, 'w', encoding='utf-8') as f:
f.write(api_key)
return True
except Exception as e:
print(f"保存API密钥失败: {e}")
messagebox.showerror("错误", f"保存API密钥失败:{str(e)}")
return False
return False
def create_ui(self):
"""创建用户界面"""
main_frame = ttk.Frame(self.root)
main_frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=8)
# 左侧配置面板
left_frame = ttk.LabelFrame(main_frame, text="参数配置", padding=12)
left_frame.pack(side=tk.LEFT, fill=tk.BOTH, padx=5, pady=5)
left_frame.config(width=300)
# API密钥部分
ttk.Label(left_frame, text="API密钥:", font=self.title_font).pack(anchor=tk.W, pady=(5, 2))
api_key_frame = ttk.Frame(left_frame)
api_key_frame.pack(anchor=tk.W, pady=(0, 5), fill=tk.X)
self.api_key_var = tk.StringVar(value=self.api_key)
self.api_key_entry = ttk.Entry(api_key_frame, textvariable=self.api_key_var,
width=20, font=self.chinese_font)
self.api_key_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 2))
ttk.Button(api_key_frame, text="保存", command=self.update_api_key, width=6).pack(side=tk.LEFT, padx=2)
ttk.Button(api_key_frame, text="注册",
command=self.open_register_page, width=6).pack(side=tk.LEFT, padx=2)
ttk.Separator(left_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=10)
ttk.Label(left_frame, text="选择模型:", font=self.title_font).pack(anchor=tk.W, pady=(5, 2))
self.model_var = tk.StringVar(value=self.models[0])
model_combo = ttk.Combobox(left_frame, textvariable=self.model_var,
values=self.models, state="readonly", width=25, font=self.chinese_font)
model_combo.pack(anchor=tk.W, pady=(0, 10), fill=tk.X)
ttk.Label(left_frame, text="系统提示词 (System Prompt):", font=self.title_font).pack(anchor=tk.W, pady=(5, 2))
self.system_prompt = scrolledtext.ScrolledText(left_frame, height=7, width=30,
font=self.chinese_font, wrap=tk.WORD)
self.system_prompt.pack(pady=(0, 10), fill=tk.BOTH, expand=True)
self.system_prompt.insert(tk.END, "你是一个专业、友善且富有创意的AI助手。\n你会用中文回答用户的问题。")
ttk.Label(left_frame, text="最大输出长度 (tokens):", font=self.title_font).pack(anchor=tk.W, pady=(5, 2))
max_tokens_frame = ttk.Frame(left_frame)
max_tokens_frame.pack(anchor=tk.W, pady=(0, 10), fill=tk.X)
self.max_tokens_var = tk.StringVar(value="16384")
ttk.Spinbox(max_tokens_frame, from_=100, to=16384, textvariable=self.max_tokens_var,
width=10, font=self.chinese_font).pack(side=tk.LEFT)
ttk.Label(max_tokens_frame, text="(100-16384)", font=self.chinese_font).pack(side=tk.LEFT, padx=5)
ttk.Label(left_frame, text="温度 (Temperature):", font=self.title_font).pack(anchor=tk.W, pady=(5, 2))
temp_frame = ttk.Frame(left_frame)
temp_frame.pack(anchor=tk.W, pady=(0, 10), fill=tk.X)
self.temperature_var = tk.StringVar(value="0.7")
temp_scale = ttk.Scale(temp_frame, from_=0, to=2, orient=tk.HORIZONTAL,
variable=self.temperature_var, command=self.update_temp_label)
temp_scale.pack(side=tk.LEFT, fill=tk.X, expand=True)
self.temp_label = ttk.Label(temp_frame, text="0.7", width=5, font=self.chinese_font)
self.temp_label.pack(side=tk.LEFT, padx=5)
ttk.Label(left_frame, text="Top P:", font=self.title_font).pack(anchor=tk.W, pady=(5, 2))
topp_frame = ttk.Frame(left_frame)
topp_frame.pack(anchor=tk.W, pady=(0, 15), fill=tk.X)
self.top_p_var = tk.StringVar(value="0.9")
topp_scale = ttk.Scale(topp_frame, from_=0, to=1, orient=tk.HORIZONTAL,
variable=self.top_p_var, command=self.update_topp_label)
topp_scale.pack(side=tk.LEFT, fill=tk.X, expand=True)
self.topp_label = ttk.Label(topp_frame, text="0.9", width=5, font=self.chinese_font)
self.topp_label.pack(side=tk.LEFT, padx=5)
button_frame = ttk.Frame(left_frame)
button_frame.pack(anchor=tk.W, pady=(10, 0), fill=tk.X)
ttk.Button(button_frame, text="清空对话", command=self.clear_conversation).pack(fill=tk.X, pady=2)
ttk.Button(button_frame, text="导出对话", command=self.export_chat).pack(fill=tk.X, pady=2)
ttk.Button(button_frame, text="导入设置", command=self.import_settings).pack(fill=tk.X, pady=2)
ttk.Button(button_frame, text="使用教程", command=self.open_tutorial_page).pack(fill=tk.X, pady=2)
ttk.Separator(left_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=10)
ttk.Label(left_frame, text="状态:", font=self.title_font).pack(anchor=tk.W, pady=(5, 2))
self.status_label = ttk.Label(left_frame, text="就绪", font=self.chinese_font, foreground="green")
self.status_label.pack(anchor=tk.W)
# 右侧主面板(包含聊天和代码选项卡)
right_frame = ttk.LabelFrame(main_frame, text="聊天与代码", padding=10)
right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=5, pady=5)
# 创建选项卡容器
self.main_notebook = ttk.Notebook(right_frame)
self.main_notebook.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
# 聊天选项卡
chat_tab = ttk.Frame(self.main_notebook)
self.main_notebook.add(chat_tab, text="聊天")
self.chat_display = scrolledtext.ScrolledText(chat_tab, height=25, width=70,
font=self.chinese_font, wrap=tk.WORD,
state=tk.DISABLED, bg="#f5f5f5")
self.chat_display.pack(fill=tk.BOTH, expand=True)
# 标签配置
self.chat_display.tag_config("user", foreground="#0066cc",
font=(self.chinese_font[0], self.chinese_font[1], "bold"))
self.chat_display.tag_config("assistant", foreground="#009900",
font=(self.chinese_font[0], self.chinese_font[1], "bold"))
self.chat_display.tag_config("thinking", foreground="#FF9500",
font=(self.chinese_font[0], self.chinese_font[1], "bold"))
self.chat_display.tag_config("error", foreground="#cc0000",
font=(self.chinese_font[0], self.chinese_font[1], "bold"))
self.chat_display.tag_config("timestamp", foreground="#999999", font=(self.chinese_font[0], 9))
# Markdown标签
self.chat_display.tag_config("bold", font=(self.chinese_font[0], self.chinese_font[1], "bold"))
self.chat_display.tag_config("italic", font=(self.chinese_font[0], self.chinese_font[1], "italic"))
self.chat_display.tag_config("code", foreground="#d63384", background="#f8f9fa", font=("Courier", 9))
self.chat_display.tag_config("heading1", font=(self.chinese_font[0], 14, "bold"), foreground="#000080")
self.chat_display.tag_config("heading2", font=(self.chinese_font[0], 12, "bold"), foreground="#000080")
self.chat_display.tag_config("heading3", font=(self.chinese_font[0], 11, "bold"), foreground="#000080")
self.chat_display.tag_config("link", foreground="#0066cc", underline=True)
# 输入区域
ttk.Label(right_frame, text="输入消息 (Ctrl+Enter 快速发送):", font=self.title_font).pack(anchor=tk.W,
pady=(5, 2))
self.input_text = tk.Text(right_frame, height=5, width=70, font=self.chinese_font, wrap=tk.WORD)
self.input_text.pack(fill=tk.BOTH, padx=0, pady=(0, 8))
self.input_text.bind("<Control-Return>", lambda e: self.send_message())
button_frame_right = ttk.Frame(right_frame)
button_frame_right.pack(fill=tk.X, padx=0, pady=0)
self.send_button = ttk.Button(button_frame_right, text="发送", command=self.send_message)
self.send_button.pack(side=tk.LEFT, padx=3)
ttk.Button(button_frame_right, text="清空输入", command=self.clear_input).pack(side=tk.LEFT, padx=3)
ttk.Button(button_frame_right, text="复制最后回复", command=self.copy_last_response).pack(side=tk.LEFT, padx=3)
def add_code_tab(self, code_content):
"""添加代码选项卡"""
self.code_tab_count += 1
tab_name = f"代码 {self.code_tab_count}"
try:
code_frame = ttk.Frame(self.main_notebook)
self.main_notebook.add(code_frame, text=tab_name)
# 创建代码显示区域
code_display = scrolledtext.ScrolledText(code_frame, font=self.code_font,
wrap=tk.NONE, bg="#2b2b2b",
fg="#f8f8f2", insertbackground="white")
code_display.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# 插入代码内容
code_display.insert(tk.END, code_content)
code_display.config(state=tk.DISABLED)
# 添加右键菜单复制功能
def copy_code():
self.root.clipboard_clear()
self.root.clipboard_append(code_content)
self.status_label.config(text="代码已复制到剪贴板", foreground="blue")
popup_menu = tk.Menu(code_display, tearoff=0)
popup_menu.add_command(label="复制代码", command=copy_code)
def show_popup(e):
try:
popup_menu.tk_popup(e.x_root, e.y_root)
finally:
popup_menu.grab_release()
code_display.bind("<Button-3>", show_popup)
except Exception as e:
print(f"添加代码选项卡出错: {e}")
def open_register_page(self):
"""打开注册页面"""
webbrowser.open(self.register_url)
def open_tutorial_page(self):
"""打开使用教程页面"""
webbrowser.open(self.tutorial_url)
def update_api_key(self):
"""更新并保存API密钥"""
new_api_key = self.api_key_var.get().strip()
if not new_api_key:
messagebox.showwarning("提示", "请输入API密钥")
return
if self.save_api_key(new_api_key):
self.api_key = new_api_key
messagebox.showinfo("成功", "API密钥已保存")
self.status_label.config(text="API密钥已保存", foreground="blue")
def update_temp_label(self, value):
"""更新温度标签"""
self.temp_label.config(text=f"{float(value):.2f}")
def update_topp_label(self, value):
"""更新Top P标签"""
self.topp_label.config(text=f"{float(value):.2f}")
def send_message(self):
"""发送消息"""
# 获取当前API密钥(从输入框获取)
current_api_key = self.api_key_var.get().strip()
if not current_api_key:
messagebox.showerror("错误", "请先填写API密钥或点击注册按钮获取密钥")
return
# 更新实例变量
self.api_key = current_api_key
if self.is_loading:
messagebox.showwarning("提示", "正在等待上一条消息的回复,请稍候...")
return
user_message = self.input_text.get("1.0", tk.END).strip()
if not user_message:
messagebox.showwarning("提示", "请输入消息")
return
self.display_message("你", user_message, "user")
self.clear_input()
self.is_loading = True
self.send_button.config(state=tk.DISABLED)
self.status_label.config(text="正在处理...", foreground="orange")
self.root.update()
threading.Thread(target=self.get_response, args=(user_message,), daemon=True).start()
def get_response(self, user_message):
"""获取AI响应(在工作线程中执行)"""
try:
self.conversation_history.append({
"role": "user",
"content": user_message
})
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
messages = []
system_prompt = self.system_prompt.get("1.0", tk.END).strip()
if system_prompt:
messages.append({
"role": "system",
"content": system_prompt
})
if len(self.conversation_history) > 1:
messages.extend(self.conversation_history[-2:])
else:
messages.extend(self.conversation_history)
data = {
"model": self.model_var.get(),
"messages": messages,
"max_tokens": int(self.max_tokens_var.get()),
"temperature": float(self.temperature_var.get()),
"top_p": float(self.top_p_var.get()),
"stream": True
}
response = requests.post(self.api_url, headers=headers, json=data, timeout=120, stream=True)
if response.status_code != 200:
error_msg = f"API错误 {response.status_code}\n"
try:
error_json = response.json()
error_msg += json.dumps(error_json, ensure_ascii=False, indent=2)
except:
error_msg += response.text
self.display_message("错误", error_msg, "error")
self.status_label.config(text="错误", foreground="red")
return
# 处理流式响应
assistant_message = ""
reasoning_content = ""
timestamp = datetime.now().strftime("%H:%M:%S")
thinking_shown = False
content_started = False
update_count = 0
self.chat_display.config(state=tk.NORMAL)
self.chat_display.insert(tk.END, f"[{timestamp}] ", "timestamp")
for line in response.iter_lines():
if not line:
continue
line = line.decode('utf-8') if isinstance(line, bytes) else line
line = line.strip()
if line == "[DONE]" or line == "data: [DONE]":
break
if line.startswith("data: "):
data_str = line[6:]
elif line.startswith("{"):
data_str = line
else:
continue
try:
chunk = json.loads(data_str)
if "error" in chunk:
error_msg = chunk.get("error", {}).get("message", "未知错误")
self.chat_display.insert(tk.END, f"流式错误: {error_msg}\n", "error")
break
choices = chunk.get("choices", [])
if not choices or len(choices) == 0:
continue
delta = choices[0].get("delta", {})
# 处理reasoning_content (思考过程)
reasoning = delta.get("reasoning_content")
if reasoning:
if not thinking_shown:
self.chat_display.insert(tk.END, "思考过程:\n", "thinking")
thinking_shown = True
reasoning_content += reasoning
self.chat_display.insert(tk.END, reasoning)
# 处理content (最终内容)
content = delta.get("content")
if content:
if not content_started:
if thinking_shown:
self.chat_display.insert(tk.END, "\n\n最终回复:\n", "assistant")
else:
self.chat_display.insert(tk.END, "AI:\n", "assistant")
content_started = True
assistant_message += content
self.chat_display.insert(tk.END, content)
# 每5个数据块更新一次UI
update_count += 1
if update_count % 5 == 0:
self.chat_display.see(tk.END)
self.root.update()
except json.JSONDecodeError:
continue
# 完成流式接收
self.chat_display.insert(tk.END, "\n\n")
self.chat_display.see(tk.END)
self.chat_display.config(state=tk.DISABLED)
self.root.update()
# 处理回复和代码块
if assistant_message:
# 提取代码块
text_without_code, code_blocks = MarkdownFormatter.extract_code_blocks(assistant_message)
# 重新格式化聊天窗口(移除代码块)
self.chat_display.config(state=tk.NORMAL)
try:
if thinking_shown:
search_text = "最终回复:\n"
else:
search_text = "AI:\n"
pos = self.chat_display.search(search_text, "1.0", nocase=True)
if pos:
pos_line, pos_col = pos.split('.')
pos_line = str(int(pos_line) + 1)
start_pos = f"{pos_line}.0"
self.chat_display.delete(start_pos, tk.END)
if text_without_code:
MarkdownFormatter.format_text(self.chat_display, text_without_code)
self.chat_display.insert(tk.END, "\n\n")
except Exception as e:
print(f"格式化错误: {e}")
self.chat_display.config(state=tk.DISABLED)
# 添加代码选项卡
if code_blocks:
for code_block in code_blocks:
self.add_code_tab(code_block)
# 保存对话历史
self.conversation_history.append({
"role": "assistant",
"content": assistant_message
})
self.status_label.config(text="就绪", foreground="green")
else:
self.display_message("错误", "未收到有效的AI响应", "error")
self.status_label.config(text="无响应", foreground="red")
except requests.exceptions.Timeout:
self.display_message("错误", "请求超时(120秒)。请检查网络连接或尝试更小的输出长度。", "error")
self.status_label.config(text="超时错误", foreground="red")
except requests.exceptions.ConnectionError as e:
self.display_message("错误", f"连接失败:{str(e)}\n请检查API地址和网络连接。", "error")
self.status_label.config(text="连接错误", foreground="red")
except Exception as e:
self.display_message("错误", f"发生错误:{str(e)}", "error")
self.status_label.config(text="未知错误", foreground="red")
finally:
self.is_loading = False
self.send_button.config(state=tk.NORMAL)
if self.status_label.cget("text") == "正在处理...":
self.status_label.config(text="就绪", foreground="green")
def display_message(self, sender, message, tag=""):
"""显示消息"""
self.chat_display.config(state=tk.NORMAL)
timestamp = datetime.now().strftime("%H:%M:%S")
self.chat_display.insert(tk.END, f"[{timestamp}] ", "timestamp")
self.chat_display.insert(tk.END, f"{sender}:\n", tag)
if tag in ["user", "error"]:
self.chat_display.insert(tk.END, f"{message}\n\n")
else:
MarkdownFormatter.format_text(self.chat_display, message)
self.chat_display.insert(tk.END, "\n\n")
self.chat_display.config(state=tk.DISABLED)
self.chat_display.see(tk.END)
def clear_conversation(self):
"""清空对话历史"""
if messagebox.askyesno("确认", "确定要清空所有对话历史吗?"):
self.conversation_history = []
self.chat_display.config(state=tk.NORMAL)
self.chat_display.delete("1.0", tk.END)
self.chat_display.config(state=tk.DISABLED)
self.status_label.config(text="对话已清空", foreground="blue")
# 清除所有代码选项卡(保留聊天选项卡)
while self.main_notebook.index("end") > 1:
self.main_notebook.forget(1)
self.code_tab_count = 0
def clear_input(self):
"""清空输入框"""
self.input_text.delete("1.0", tk.END)
def copy_last_response(self):
"""复制最后一条AI回复"""
if self.conversation_history:
for i in range(len(self.conversation_history) - 1, -1, -1):
if self.conversation_history[i]["role"] == "assistant":
message = self.conversation_history[i]["content"]
self.root.clipboard_clear()
self.root.clipboard_append(message)
self.status_label.config(text="已复制到剪贴板", foreground="blue")
return
messagebox.showinfo("提示", "没有可复制的AI回复")
def export_chat(self):
"""导出对话为文本文件"""
if not self.conversation_history:
messagebox.showwarning("提示", "没有对话可导出")
return
file_path = filedialog.asksaveasfilename(defaultextension=".txt",
filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")])
if file_path:
try:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(f"对话导出 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
f.write(f"模型: {self.model_var.get()}\n")
f.write("=" * 60 + "\n\n")
for msg in self.conversation_history:
sender = "用户" if msg["role"] == "user" else "AI"
f.write(f"{sender}:\n{msg['content']}\n\n")
messagebox.showinfo("成功", f"对话已导出到:{file_path}")
except Exception as e:
messagebox.showerror("错误", f"导出失败:{str(e)}")
def import_settings(self):
"""从JSON文件导入设置"""
file_path = filedialog.askopenfilename(filetypes=[("JSON文件", "*.json"), ("所有文件", "*.*")])
if file_path:
try:
with open(file_path, 'r', encoding='utf-8') as f:
settings = json.load(f)
if "api_key" in settings:
self.api_key_var.set(settings["api_key"])
if "system_prompt" in settings:
self.system_prompt.delete("1.0", tk.END)
self.system_prompt.insert(tk.END, settings["system_prompt"])
if "max_tokens" in settings:
self.max_tokens_var.set(str(settings["max_tokens"]))
if "temperature" in settings:
self.temperature_var.set(str(settings["temperature"]))
if "top_p" in settings:
self.top_p_var.set(str(settings["top_p"]))
if "model" in settings and settings["model"] in self.models:
self.model_var.set(settings["model"])
messagebox.showinfo("成功", "设置导入完成")
except Exception as e:
messagebox.showerror("错误", f"导入失败:{str(e)}")
if __name__ == "__main__":
root = tk.Tk()
app = ChatBotUI(root)
root.mainloop()
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)