第4章 第一个项目:命令行 ChatBot
目标概述
本章我们将开发一个命令行聊天机器人(ChatBot),能够在终端中读取用户输入的问题,并返回回答。对于零基础的你,这是第一个完整的小项目!通过实现这个 CLI(Command Line Interface)应用,你将学会:如何与用户交互(接收输入、打印输出)、如何调用现有AI服务获取回答,以及如何将这些部分组装成一个可以循环对话的程序。完成本章后,你可以在本地终端运行你的ChatBot,与之对话问答。
主要知识点
-
CLI 程序输入输出:使用 Python 内置的 input() 函数获取用户在终端输入的字符串,并使用 print() 将结果输出到终端。我们会在循环中反复询问,实现持续对话。
-
OpenAI API 调用:为了让ChatBot变得真正智能,我们将利用现成的 AI 接口(OpenAI 提供的 ChatGPT 模型)来生成回答。你将学习如何安装和使用 OpenAI 官方的 Python SDK(openai 库)[53]。包括:设置 API 密钥、调用 ChatGPT 接口并获取回复文本。
-
API Key 配置:OpenAI API 调用需要验证身份,所以必须提供 API Key。我们会讨论如何安全地将 API 密钥存储在代码之外(例如通过环境变量)并在程序中读取,避免把密钥写死在代码里。
-
HTTP 请求基础:作为背景知识,OpenAI API 本质是通过 HTTP 请求发送用户提示,接收 AI 回复。虽然我们用封装好的 SDK,不需手写HTTP,但理解背后有一个请求->响应过程,有助于调试和错误处理。常见问题如网络错误、密钥无效等需要排查。
-
程序结构:将上一章基础融会贯通:用循环持续对话、用函数封装 API 请求、用条件判断实现退出命令等。我们还会添加一些简单容错,比如当API没有返回正常结果时的处理。
-
AI 模型选择:OpenAI 提供不同模型,如 GPT-3.5(gpt-3.5-turbo)和 GPT-4。我们将使用经济实惠且强大的 GPT-3.5。通过参数可以指定模型和回复长度、温度等[54]。理解这些参数如何影响回答风格也是拓展知识点。
完整代码
首先,你需要在 OpenAI 官网注册账户并获取一个 API Key[55]。注册后在 Dashboard 的API Keys页面生成新密钥(形如sk-...开头的字符串)。请妥善保管,不要泄露。然后在本地设置环境变量,例如在Cursor终端运行:
export OPENAI_API_KEY="你的API密钥"
这样我们的程序可以从系统环境读取密钥,而不用把它硬编码在代码里。
接下来安装 OpenAI 的 Python 包。在 Cursor 中打开终端执行:
pip install openai
(确保你的Python环境联网,成功后才能调用OpenAI接口。)[53]
现在,新建 chatbot_cli.py 文件,输入以下代码:
import os
import openai
# 从环境变量获取 API Key,并配置 OpenAI 库
openai.api_key = os.getenv("OPENAI_API_KEY")
if openai.api_key is None:
raise Exception("未找到 OpenAI API 密钥,请设置环境变量 OPENAI_API_KEY")
# 定义一个函数,调用 OpenAI 接口获取回答
def get_answer(question):
try:
response = openai.Completion.create(
engine="text-davinci-003", # 使用GPT-3文本生成模型
prompt=question,
temperature=0.7, # 回答随机性, 0~1,值越高越发散
max_tokens=1024, # 单次回答最多的token数
n=1, # 只需返回一个回答
stop=None # 不设置停止符
)
# 提取生成的文本答案
answer = response.choices[0].text.strip()
return answer
except Exception as e:
# 捕获API调用异常,打印并返回一个错误消息
print("调用 OpenAI API 出错:", e)
return "【出错了,请稍后再试】"
# 主循环:不断读取用户输入并输出AI回答
print("欢迎使用 CLI ChatBot!输入你的问题(或输入 exit 退出):")
while True:
try:
user_input = input(">> ") # 提示符 >> 等待用户输入
except (EOFError, KeyboardInterrupt):
# 捕获 Ctrl+D 或 Ctrl+C 引发的异常,退出程序
print("\n聊天已结束。")
break
if user_input.strip().lower() in ("exit", "quit"):
print("聊天已结束。")
break
if user_input.strip() == "":
# 若输入空字符串,则提示重新输入
continue
# 获取回答并打印
reply = get_answer(user_input)
print("Bot:", reply)
代码说明:
- 导入 os 和 openai 模块。使用 os.getenv 获取环境变量 OPENAI_API_KEY[56]并赋值给 openai.api_key 完成配置。如果获取不到,抛异常提醒用户没有配置密钥。
- get_answer(question) 函数中,调用了 openai.Completion.create 方法[54]向 OpenAI 请求完成任务:我们传入 engine="text-davinci-003" 来指定使用 Davinci 模型(GPT-3的一个大型模型)[57]。prompt 参数就是我们的问题文本,API 将据此生成后续内容。temperature 控制随机性(0表示确定性回答,1表示高随机性),max_tokens限制回答长度。函数返回 API 返回的第一个回答文本(去除首尾空白)。如果调用过程发生异常(可能是网络问题、密钥问题等),捕获并返回一个出错提示,程序不会崩溃。
- 主程序使用 while True 无限循环,不断 input(">> ") 读取用户输入[47]。input() 在终端等待用户输入文本,按回车确认。我们对特殊输入做处理:如果用户输入 "exit" 或 "quit"(不区分大小写)则退出循环并结束程序;如果输入空行则忽略继续问。
- 对于正常问题,调用我们定义的 get_answer 获取 AI 回复,然后打印输出格式为 Bot: 回答内容。这样每轮对话就完整呈现。
- 我们还捕获了 EOFError 和 KeyboardInterrupt 异常[47],它们分别对应用户按 Ctrl+D (Linux/macOS) 或 Ctrl+Z (Windows) 发送EOF,或者按 Ctrl+C 终止程序。这两种情况都优雅地退出循环,打印结束语。
运行程序:在 Cursor 终端中执行:
python chatbot_cli.py
看到提示“欢迎使用 CLI ChatBot!输入你的问题(或输入 exit 退出):”。这表示机器人已启动,等待你的输入。试问它一些问题,比如:
>> 今天天气怎么样?
Bot: 很抱歉,我无法直接获取实时天气... (示例回答)
Python和Java有什么区别?
Bot: Python是动态类型的解释型语言,而Java是静态类型的编译型语言...(示例回答)
当你输入 exit 后,程序会退出。恭喜,你已经创建了一个可以和你聊天的机器人!它能回答各类问题,背后是强大的OpenAI模型在支持。
[58]上面的截图(代码中注释)展示了程序的主要逻辑,即不断读取用户输入并调用API返回答案的循环流程。可以看到,这和前面我们分析的Pseudo-code一致:Prompt -> Answer -> Loop。注意:每次调用 API 都会耗费一定时间和费用(OpenAI根据token计费)。在免费试用额度用尽或网络异常时,API可能报错,我们在代码里做了简单处理输出错误信息。实际产品中可以更细致地根据异常类型采取重试等机制。
操作截图或流程
-
启动与问答:第一张截图展示了终端运行程序并交互的过程:用户每输入一行问题,下一行Bot给出回答。这体现了 input() 和 print() 的交替工作流程。
-
API 调用流程:第二张图是一个示意流程图,描述我们程序内部如何运作:用户输入被传递给 get_answer -> 通过OpenAI库发送HTTP请求到服务器[54]-> OpenAI返回JSON响应 -> openai库解析出文本答案 -> 程序获取该答案打印给用户。这个过程用户不可见,但作为开发者理解其异步交互非常重要。
-
异常示例:第三张截图示例如果用户断网或API密钥无效时,程序捕获异常打印"调用 OpenAI API 出错:"及错误原因,然后Bot回答【出错了,请稍后再试】。这对应我们的 except 分支执行结果。通过这种设计,我们的聊天机器人在API故障时不会直接崩溃退出,而是给出友好的错误提示。
(由于需要API密钥和联网,此处不附带实际运行截图。你可以自行获取密钥运行,观察终端交互效果,并确保关闭日志避免泄露密钥。)
常见错误
-
API Key 未设置或错误:如果 os.getenv("OPENAI_API_KEY") 得到 None,我们显式抛出了异常停止程序[56]。解决:正确设置环境变量。例如在 Cursor 终端运行 export OPENAI_API_KEY="sk-...你的密钥..."(Windows下使用 set 命令),或将密钥写入 .env 文件并在代码中用 dotenv 加载[59]。密钥要确保不包含多余引号或空格。
-
网络错误/超时:调用 OpenAI 时可能遇到网络不稳定,常见异常例如 openai.error.APIConnectionError 或请求超时。我们的 except Exception as e 会捕获所有异常并打印[60]。你可以根据需要对不同类型的异常分别处理,例如遇到超时可以重试一次。如果频繁超时,检查网络或OpenAI服务状态。
-
请求被拒:OpenAI可能因为各种原因拒绝请求,例如提示内容违规或用量超限,会返回 openai.error.InvalidRequestError 等。这种情况 e 会包含详细信息。我们的代码将打印错误信息并返回通用错误回复。建议在调试时关注终端打印,比如 "调用 OpenAI API 出错:Your request was rejected..." 等,然后改进。例如如果出现 "maximum context length exceeded" 说明问题太长或对话累积过多,可以考虑截断提示或降低 max_tokens。
-
模型选择错误:指定模型名称不正确会引发错误,如写成不存在的 engine="text-davinci-004"。确保模型名称正确拼写。OpenAI的ChatGPT系列新接口需要使用 openai.ChatCompletion.create 不同的调用,如果你尝试改用 gpt-3.5-turbo 就要调整用 ChatCompletion 并提供 messages 列表,而不是 Completion+prompt[57]。本示例用 text-davinci-003 完全足够满足问答。
-
编码问题:终端可能遇到 Unicode 字符无法显示,比如 Bot回答包含特殊符号,在 Windows 控制台可能乱码。这不属于代码错误,可通过设置终端编码或使用Cursor内置终端(通常UTF-8)避免。
-
退出判断:我们的退出判断使用了 user_input.strip().lower() 来标准化用户输入,如果输入 Exit 带大写或两边有空格都能识别。如果你忘了这些处理,直接比较 "exit" 可能漏掉用户输入 "Exit" 导致无法退出。这个细节体现了处理用户输入的严谨性。
延伸思考
-
丰富 ChatBot 功能:目前Bot每次对话不“记得”之前的内容。如果你连续问:“小明是谁?” 然后再问 “他今年多大?”,Bot 未必知道第二问的“他”指小明,因为我们每次调用API只传了当前单句问题。思考如何增加对话上下文记忆?(提示:可以将之前对话拼接进 prompt,例如 "小明是谁?\nAI: ...(回答)...\n用户:他今年多大?\nAI:" 传给API)。这样模拟多轮对话,但要注意长度不能无限增长。)
-
自定义知识:OpenAI API默认回答基于训练的大量通识。有时我们想让Bot回答特定领域或我们的自定义内容,比如公司内部文档问答。这需要在 prompt 中加入context或使用后续章节将Bot封装成服务与其它工具结合。你可以尝试让Bot回答一些你熟悉领域的问题,评估答案准确性。对于明显不正确的回答,想想如何改进prompt,比如加上 "如果不知道就回答不知道" 之类的要求。
-
调用成本与优化:OpenAI API按 token 计费,每个请求耗费 prompt和生成的token。我们的实现较简单,未做优化。如果把Bot部署给很多人用,要考虑减少不必要的token消耗。例如,如果用户问题非常长,实际上OpenAI模型对太长输入可能反而效果变差,可以截断或总结用户意图后再提问。OpenAI还有流式输出接口,可以边生成边输出,这样可改善用户体验(后面FastAPI章节会谈及)。
完成这个CLI ChatBot,你已经初尝 AI 应用的开发!下一章,我们将继续升级,将这个聊天功能封装为一个可供其它程序/前端调用的 API 服务。