☰
Python+OpenAI API:从零构建带记忆的聊天机器人实战
2026/10/21 14:27:48 网站建设 项目流程

我第一次尝试用 Python 调用 OpenAI API 做聊天机器人的时候,心态特别简单:只要能发一句话过去,得到一句回复,就算成功了。可真上了手才发现,很多教程只告诉你调用接口那三五行代码,却不会告诉你上下文怎么存、超时怎么处理、密钥怎么放、参数怎么调。这篇文章我完全用自己实操的视角来讲,从安装依赖开始,到一个带记忆、能调参数、还能包装成 Web 接口的聊天机器人。适合刚入门 Python、想把 API 真正用起来的人。如果你已经能调通接口,可以直接跳到后面看上下文管理和错误处理,那部分我踩过的坑比第一行代码多得多。

1. 先理清“智能”从哪来:聊天机器人的最小原理

1.1 别把它想成“两个人对话”:本质是补全一段文本

很多初学者容易把聊天机器人想复杂,觉得是不是要在本地训练一个模型,或者要写大量的语义分析代码。其实用 Python 和 OpenAI API 做聊天机器人,我们的代码只是在做一件事:把对话文本整理好,交给接口,然后拿到返回的文本。模型背后的“智能”来自海量语料训练出的参数,而我们这边不需要理解那些参数。

我用“文本接龙”来类比:你给模型一句话,它根据这句话预测下一个最可能出现的词,然后接着预测下一个,直到形成一段完整回复。所以你会发现,同样一句话换个措辞,结果可能差异很大,因为模型是在做概率计算,不是在查答案。

这个认知帮我解决了很多困惑。比如有段时间我发现机器人总是“忘记”前面聊过的内容,于是疯狂在代码里找状态管理的 bug。后来才反应过来,模型本身没有记忆,它每次能看到的只是我在 messages 数组里重新发给它的全部文本。理解了这一点,后面所有上下文管理方案都是围绕“每轮请求前把该带的文本都带上”来设计的。

1.2 一次调通最小请求需要理解哪些参数

先给一个最小可运行代码,这是整个项目的地基。新版官方 Python 库的调用方式已经统一成下面这样:

from openai import OpenAI client = OpenAI(api_key="sk-你的密钥") response = client.chat.completions.create( model="gpt-3.5-turbo", messages=[ {"role": "user", "content": "你好,请用一句话介绍你自己"} ] ) print(response.choices[0].message.content)

这段代码里有三个最关键的点。

第一个是 model,也就是模型名称。不同模型能力和价格差距很大,刚开始不要追高,选一个稳定、性价比合适的对话模型先跑通流程即可。

第二个是 messages,这是一个列表,里面每个元素都有 role 和 content。role 表示这句话是谁说的,常见的有 system、user、assistant,后面会专门讲。

第三个是响应结构。返回的是一个对象,我们要的内容在response.choices[0].message.content里。我用过很长一段时间的旧版接口,字段名是text,新版统一改成了message.content。如果你以后看到老教程报类型错误,多半是版本差异。

1.3 OpenAI API 的计费逻辑:初学阶段最容易忽略的成本

聊机器人绕不开成本。OpenAI API 按 token 计费,输入和输出都要收费。所谓 token,简单理解就是模型处理文本的切分单位。英文里 1 个 token 通常对应 0.75 个左右单词;中文比较特殊,一个汉字经常被拆成 1 到 2 个 token。

我见过不少人第一版代码就把所有历史对话原封不动地重复发送,结果聊几十轮之后,每次请求携带的文本越来越多,输入 token 快速增长,价格也随之变高。更麻烦的是,超出模型的上下文窗口后,接口会直接报错,而不是自动帮你删旧内容。

所以从一开始就要有成本意识:模型有固定的上下文长度上限,我们传给它的 messages 只是我们想让它“看到”的内容,不是越多越好。后面第三部分讲的上下文管理,本质上就是在成本和效果之间做平衡。

2. 环境准备与密钥管理的正确姿势

2.1 创建虚拟环境和安装依赖

我建议任何 Python 项目都先建虚拟环境,不要图省事直接装到全局。虚拟环境可以把依赖隔离在项目目录里,避免不同项目的包版本互相打架。我自己有一次就是因为全局环境里的旧版 openai 库,导致新代码一直报参数错误,排查了半天。

创建环境的命令很简单:

python -m venv venv source venv/bin/activate # Windows 下是 venv\Scripts\activate

激活后安装两个库就够起步:

pip install openai python-dotenv

openai是官方 SDK,python-dotenv用来读取密钥配置文件。版本方面,只要能用OpenAI()这类新写法,就是 1.x 的版本。如果安装完发现还是旧写法openai.ChatCompletion.create,说明库版本太老,建议升级。

2.2 API Key 不该出现在代码里

很多教程为了方便,会把密钥直接写在代码里。跟我一样犯过这个错的人应该不少:代码提交到仓库后,密钥就泄露了,然后被别人调用出了不少账单。

正确的做法是用环境变量,或者项目根目录放一个.env文件:

OPENAI_API_KEY=sk-你的密钥

然后在代码里这样读取:

import os from dotenv import load_dotenv load_dotenv() api_key = os.getenv("OPENAI_API_KEY") client = OpenAI(api_key=api_key)

.env文件一定不要提交到代码仓库。在项目根目录建一个.gitignore,把.env放进去,这是最基本的自我保护。此外,密钥本身是敏感凭据,尽量只配最小权限,不要使用超出需求的账户权限。

2.3 第一次调通的最小验证脚本

环境准备好之后,建议先写一个最简脚本验证密钥和网络都通,再开始写复杂逻辑。我的验证脚本大概长这样:

import os from dotenv import load_dotenv from openai import OpenAI load_dotenv() client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) resp = client.chat.completions.create( model="gpt-3.5-turbo", messages=[{"role": "user", "content": "ping"}], max_tokens=10, ) print(resp.choices[0].message.content)

这里故意加了一个max_tokens=10,一方面节省调用成本,另一方面验证返回是否正常。如果报 401,基本是密钥有问题;如果报网络错误,就先检查网络环境和连通性。这一步跑通之后,后面所有代码都可以在这个脚本基础上扩展。

3. 把对话历史的“记忆”交给 messages 数组

3.1 role 与 messages 的结构说明

messages 是聊天机器人的核心数据结构。三个最常用的 role 分别是:

role来源作用
system开发者设定机器人的角色、性格、回复规则
user用户用户输入的提问或指令
assistant机器人机器人之前的回复

结合一个例子看:

messages = [ {"role": "system", "content": "你是一个喜欢用短句回答问题的助手"}, {"role": "user", "content": "今天天气怎么样?"}, {"role": "assistant", "content": "我无法获取实时天气,但可以给你查天气的建议。"}, {"role": "user", "content": "那帮我查一下"}, ]

模型在生成下一次回复前,会通读整个 messages 列表。所以 assistant 的历史回复也必须放进去,否则模型不知道自己之前说过什么,对话就会显得断片。记住一个原则:messages 不是单次请求参数,它代表你希望模型看到的完整“对话剧本”。

3.2 多轮对话的两种拼接方式

明白了 messages 的运行原理,多轮对话实现起来就是在每次请求前追加 user 内容,拿到回复后追加 assistant 内容。控制台版的循环可以这样写:

client = OpenAI() messages = [] while True: user_input = input("你:") if user_input.lower() == "exit": break messages.append({"role": "user", "content": user_input}) response = client.chat.completions.create( model="gpt-3.5-turbo", messages=messages, ) reply = response.choices[0].message.content messages.append({"role": "assistant", "content": reply}) print("机器人:", reply)

这个版本虽然能跑,但有个隐患:messages 会无限增长。实际项目中我通常会在每次 new 请求前做一次“消息窗口管理”,只保留最近的若干轮,而不是全量发送。

常见的做法是两种:第一,固定只保留最近 N 条消息;第二,按 token 数估算,超过阈值就删最早的消息。第一种实现简单,第二种更精准,适合会话很长的场景。初期项目用第一种就够了:

MAX_ROUNDS = 10 def trim_history(history): return history[-MAX_ROUNDS * 2:]

这里乘 2 是因为每条会话包含 user 和 assistant 两条消息。

3.3 把“记忆”截断:上下文窗口管理

每个模型都有最大上下文长度,比如常见的对话模型能容纳的 token 数是固定的。如果你把大量历史文本都塞进去,一旦超过限制,接口会返回 length 错误。

要精确计算 token,可以用官方配套的tiktoken库:

import tiktoken enc = tiktoken.encoding_for_model("gpt-3.5-turbo") tokens = enc.encode("这是一个测试文本") print(len(tokens))

不过对大多数个人项目,不需要每个请求都去精确计算 token。我习惯用一个更粗的策略:每次请求前,把所有 messages 文本拼接起来,统计大概的字符数,然后按“1 个汉字约 1.5 个 token、1 个英文单词约 1.3 个 token”的经验来估算。超出预设阈值时,保留 system 消息,然后从最早的对话开始丢弃。

这里有一个容易被坑的点:system 消息最好永远放在列表第一位,不要让它被当成普通历史消息删掉。我刚开始写裁剪逻辑时,直接把整个列表前几项删掉,结果人设消息丢了,机器人的语气一下子变得很奇怪。后来才把 system 单独拆出来保护。

4. 从能聊到聊得好:角色设定与生成参数调优

4.1 system prompt 就是产品的“人设”

如果只是调通接口,那聊天机器人只能算能跑,离“好用”还有距离。第一个明显的提升点在 system prompt。

我最早做客服 Demo 时,只把用户提问放进 messages,结果机器人回答风格飘忽不定,有时候长篇大论,有时候只回几个字。后来我在 system 里明确写了规则,效果立刻稳定很多:

system_prompt = { "role": "system", "content": "你是一个靠谱的客服助手。语气友好但不过度热情。回答尽量控制在50字以内。如果不知道答案,直接说不知道,不要编造。" }

这段消息并不是每一轮都由用户重复输入的,但它会跟随每次请求一起发给模型。所以它相当于整个对话的“总编辑规则”,设置一次就能影响后面所有回复。实际调优时,可以把你想约束的行为都写进去,比如用词风格、回复长度、是否允许反问、敏感问题怎么回避。

4.2 temperature/top_p/max_tokens 到底影响什么

生成参数是另一个能明显改变体验的地方。最常用的是 temperature,它控制随机性:值越低,模型越倾向于选择高概率的词,回答更稳定;值越高,输出更多样,但也更容易跑偏。做客服、工具类机器人我一般设 0.2 到 0.4;做创意写作、头脑风暴可以调到 0.8。

top_p 是另一个采样参数,和 temperature 类似,表示从概率累计达到 p 的词里采样。官方文档建议不要同时大幅调整两个参数,我先保持 top_p 默认,主要调 temperature。

max_tokens 用来限制单次回复的最大 token 数。这里要注意的是,它限制的是输出长度,不是思考长度。如果你的机器人需要长回答,但 max_tokens 设太小,句子会被硬生生切断,看起来就像没说完整。我建议根据场景估算,比如要输出 200 个汉字,给 250 到 300 token 会比较稳。

我常用的参数组合如下:

场景temperaturemax_tokens备注
客服问答0.2150求稳
闲聊机器人0.7300有点随机性
文案生成0.9500接受发散

4.3 流式输出:从“等待十秒”到“逐字显示”

第一次调用非流式接口时,如果模型生成的文本很长,你会明显感觉页面或终端卡住了,必须等全部生成完才输出。体验很生硬。OpenAI API 支持流式返回,可以把结果一点点传回来,效果更接近真实打字。

代码如下:

stream = client.chat.completions.create( model="gpt-3.5-turbo", messages=messages, stream=True, ) for chunk in stream: delta = chunk.choices[0].delta if delta.content: print(delta.content, end="")

注意chunk.choices[0].delta.content和普通响应里的message.content不同。很多新人第一次用流式接口时还在取message,就报 AttributeError。所以看到流式输出代码,一定要用对字段。

流式输出除了体验更好,还能让用户提前感知系统在正常响应,不会以为卡死了。在 Web 场景下,可以先把这些内容推到前端。

5. 上线前必须处理的三类异常:超时、限流、格式变动

5.1 网络超时和重试:不重试等于自找麻烦

调用远程接口,网络波动是家常便饭。第一次上线时我天真地以为公网接口会一直稳定,直到某次请求卡了 30 秒后直接抛异常,用户这边看到的就是机器人没反应。后来我统一给客户端设置了较长的超时时间,并加了重试逻辑。

新版 openai 库默认会做一定次数的重试,但依赖关系多,不如自己控制清晰。我常用的重试方式是捕获连接类异常和超时异常,按指数退避等待:

import time from openai import OpenAI, APITimeoutError, APIConnectionError client = OpenAI(timeout=30.0) def request_with_retry(messages, retries=3): for attempt in range(retries): try: response = client.chat.completions.create( model="gpt-3.5-turbo", messages=messages, ) return response except (APITimeoutError, APIConnectionError): if attempt == retries - 1: raise wait = 2 ** attempt time.sleep(wait)

这样做的好处是:第一次超时后等 2 秒再试,第二次等 4 秒再试,不会一失败就崩溃。不过要注意,只有确认请求没有成功返回时才重试。如果请求已经到达服务端并触发了生成本,由于网络原因你这边没收到响应,重试就可能造成重复扣费。对普通项目来说,这个概率不算高,但如果做商业产品,需要设计“请求 ID”之类的幂等机制。

5.2 限流(429)与并发:比想象中更容易碰到

很多新人只在文档里见过“限流”,直到多线程并发调用后才真正踩到。OpenAI 接口会针对并发和每时间窗口的 token 总量做限制。一旦超了,就返回 429 错误。

我第一次做并发测试时,开了几个线程同时调用,结果不断看到 RateLimitError。处理限流不能简单无脑重试,因为如果是 token 配额被耗尽,等几秒也不一定能恢复。我的做法分成三步:第一,设置单请求重试;第二,控制整体并发数;第三,对高频重复问题做缓存。

简单场景里可以用信号量限制并发:

import threading semaphore = threading.Semaphore(3) def limited_request(messages): with semaphore: response = client.chat.completions.create( model="gpt-3.5-turbo", messages=messages, ) return response

如果你的机器人是单用户循环调用,基本不会碰到并发限流;一旦要通过 Web 服务对外提供接口,并发控制就绕不开了。

5.3 接口返回结构变化的防御:别盲赌字段存在

OpenAI SDK 返回的结构大部分时间很稳定,但接口升级时偶尔有微调。我见过不止一次线上代码在取response.choices[0].message.content时写得太自信,结果某一天返回里没有这句,直接 KeyError 或空指针。

现在我会在取结果前做一下保护:

msg = None if response.choices and response.choices[0].message: msg = response.choices[0].message.content if not msg: print("接口返回为空,可能被内容过滤或参数配置导致") else: print(msg)

还有一个容易被忽略的情况:模型输出可能因为内容过滤而返回空字符串,特别是用户输入涉及安全敏感内容时。如果直接把空字符串当成正常回答发回前端,用户会以为机器人坏了。这种时候最好给一个默认兜底回复,比如“这个问题我暂时无法回答”。

6. 把机器人封装成 Web 接口:从脚本到服务的最后一公里

6.1 用 FastAPI 暴露一个 /chat 接口

控制台脚本玩得差不多了,下一步通常是想做一个网页版聊天界面。最省事的方案是用 FastAPI 把聊天逻辑包装成 HTTP 接口。

先安装:

pip install fastapi uvicorn

再写一个最简单的接口:

from fastapi import FastAPI from pydantic import BaseModel from openai import OpenAI app = FastAPI() client = OpenAI() class ChatRequest(BaseModel): message: str history: list[dict[str, str]] = [] @app.post("/chat") def chat(req: ChatRequest): messages = req.history + [{"role": "user", "content": req.message}] response = client.chat.completions.create( model="gpt-3.5-turbo", messages=messages, ) reply = response.choices[0].message.content return {"reply": reply}

这里把用户传来的历史列表直接透传,没有做裁剪,仅适合原型验证。生产环境里,history 不应该完全由客户端决定,最好在服务端按用户会话 ID 存储,并且做长度限制。

6.2 同步请求与异步事件循环的坑

上面的代码写成def chat,FastAPI 会把它放到线程池里执行,不会阻塞主循环。但如果你把它改成async def chat,里面又直接调用同步的client.chat.completions.create,那这就会阻塞事件循环,并发能力会变差。

我之前第一次用 FastAPI 时就是这个原因:接口看起来正常,但多几个请求同时进来,响应时间明显变长。正确做法是用异步客户端:

from openai import AsyncOpenAI from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() client = AsyncOpenAI() class ChatRequest(BaseModel): message: str history: list[dict[str, str]] = [] @app.post("/chat") async def chat(req: ChatRequest): messages = req.history + [{"role": "user", "content": req.message}] response = await client.chat.completions.create( model="gpt-3.5-turbo", messages=messages, ) return {"reply": response.choices[0].message.content}

把接口跑起来:

uvicorn main:app --reload

如果前端页面不在本机,还会遇到跨域问题。FastAPI 里加一下中间件就行:

from fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], )

注意allow_origins=["*"]只适合本地调试,线上要收紧成你自己的域名列表。

6.3 上线后我会怎么继续优化

如果这个机器人要正式服务多用户,我后续会优先做三件事。第一,用 Redis 按用户 ID 保存历史消息,把“谁聊过什么”从客户端手里收回来;第二,把接口改成流式响应,让前端体验更好;第三,给每个用户加上调用频率限制,防止某个用户刷爆 token 配额。

除此之外,日志记录也值得一开始就做好。每次请求记录模型、输入 token、输出 token、耗时、是否重试,这些数据在排查问题和评估成本时特别有用。我见过一个项目上线后一直感觉费用高,却分析不出原因,后来加了两天日志,才发现是某个循环里重复发送了整段历史。

最后分享一个实操体会:构建聊天机器人最大的成本不是代码,而是对接口行为边界的理解。当你清楚了 messages 的构造方式、token 的计算逻辑、参数对输出的影响、异常场景的应对,剩下的就是用业务逻辑把 API 组织起来。我建议你按本文顺序把最小版跑通,再逐步加记忆、加参数调优、加 Web 封装。每加一层,你对“智能”的掌控深一分,后面遇到问题也能更快定位。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询