ChatGPT 根本没魔法!一个 while 循环 + 一个列表,我做出了聊天机器人
引言
小明上周转通了第一次 API 调用,兴奋了没两天就发现一个问题——
他问 AI:”我有十个苹果?吃掉一个还有几个?”AI 回答了。他接着问:”我原来有几个苹果”AI 回:”不知道原来苹果数?请补充你的需求。”
小明愣了。这不是接着上一句问的吗?
他去找老张:”这 AI 是不是有健忘症?”
老张笑了:”不是健忘症,是根本没有记忆。你每次调 API,对它来说都是全新的一次对话。它不知道你上一句问了什么。”
“那 ChatGPT 怎么能连续聊?”
“因为 ChatGPT 每次都把之前聊过的所有消息打包传给大模型。你以为它在’记住’,其实是它在’复读’——把历史消息全部重新喂一遍。”
小明恍然大悟。老张说:”今天我就带你实现一个能连续对话的命令行聊天机器人。核心就一句话——维护一个 messages 列表,每轮把历史消息全部传过去。“
大模型的”记忆”从哪来
老张先在白板上画了张图:
“看到了没?”老张说,”每一轮对话,你发给 API 的 messages 列表越来越长。大模型看到完整的历史,就能接上下文。这个列表就是你这个聊天机器人的’记忆’。”
小明问:”那这个列表一直涨,不会爆吗?”
“会。”老张说,”这是多轮对话最核心的问题,待会儿讲。先把基础版跑通。”
多轮对话核心代码
老张说:”配置和上次一样,看核心文件。这次就一个文件,不到 40 行。”
老张说:”别看代码短,每一行都有讲究。我逐段给你讲。”
逐段拆解
第一步:初始化历史列表
“这个 history 列表就是整个对话的’记忆容器’。程序启动时只有一条 system 消息——给 AI 定人设。”
“这里写的是’你是一位简洁的助理’,意思是让 AI 回答简短点,别长篇大论。你可以改成任何你想要的角色——’你是一个 Python 编程专家”你是一个温柔的心理咨询师’,都行。”
小明问:”这个 system 消息每轮都要传?”
“对。它放在 history[0],每轮请求都会跟着一起发过去。相当于每次都提醒 AI ‘你是谁’。”
第二步:循环接收输入
“while True 死循环,不断等用户输入。input("你:") 在终端显示 你: 提示符,等用户敲回车。”
“退出逻辑三道防线——输入 quit、exit、/quit 都能退出,大小写不敏感。输入空行也直接跳过,不会发空消息给 API 浪费 token。”
第三步:累积消息,发起请求
“这是核心中的核心。”老张加重了语气,”先把用户的话追加到 history,然后把整个 history 传给 API。”
“注意,传的是 messages=history,不是 messages=[{"role": "user", "content": user_input}]。这就是多轮对话和单轮对话唯一的区别——你传给 API 的消息列表包含了所有历史。“
第四步:保存 AI 回复,形成闭环
“拿到 AI 的回复后,必须追加到 history。这一步很多人会忘——只打印了回复,没存进历史。结果下一轮请求时,AI 不知道自己上一轮说了什么,上下文就断了。”
“or "" 是防御性写法——万一 API 返回 None(极端情况),不会报错。”
老张在白板上画了个闭环:
“四步循环,就是这个 while 循环的全部。”
跑起来看看
小明运行了程序:
小明瞪大眼睛:”它记住了!第二三轮问它都知道!”
“不是它记住了,是你每次都告诉它了。”老张说,”你在第三轮问’我多大了’的时候,你发给 API 的 messages 列表长这样:”
“它看到第一轮你就说了’今年 25 岁’,当然知道你多大。记忆不是大模型的能力,是你把历史喂给它的结果。“
小明若有所思:”那如果聊了一百轮,这个列表岂不是有一百多条?”
“对,而且每条越来越长。这就是接下来要说的问题。”
深入理解消息列表
老张喝了口水,说:”基础用法你清楚了,但 messages 列表远不止’攒一堆消息传过去’这么简单。这里面的细节,决定了你的聊天机器人到底是’能聊’还是’好聊’。”
消息的解剖结构
“先看一条消息长什么样。你平时写的是这样:”
“但完整的一条消息,其实可以包含更多字段:”
老张说:”role 和 content 是必填的,name 是可选的——用在多用户场景里,告诉 AI 这条话是谁说的。比如多人聊天室,你可以标记不同用户的发言。”
“但日常开发 90% 的情况,只需要 role + content 两个字段就够了。”
四种角色,不是三种
小明说:”我上次学的是 system、user、assistant 三种角色,还有别的?”
“还有一个 tool。”老张在白板上画了张完整表:
| role | 谁说的 | 怎么用 | 能不能省 |
|---|---|---|---|
system |
你给 AI 的指令 | 列表第一条,定人设、定规则 | 可省,但不建议 |
user |
用户说的话 | 每轮追加,用户实际输入 | 必须有 |
assistant |
AI 的回复 | 每轮追加,保存历史回复 | 多轮对话必须有 |
tool |
工具函数的返回结果 | Function Calling 时用 | 调函数时才有 |
“前三种你已经在用了。第四种 tool 是 Function Calling 专属——当你让 AI 调用一个函数(比如查天气),函数返回的结果要包成 {"role": "tool", "content": "成都今天35℃"} 塞回 messages 列表。AI 看到这个结果后才会给你最终回复。”
“这个待会儿 Function Calling 那节课会细讲,现在知道有这四种就行。”
消息顺序的硬规则
老张说:”messages 列表不是随便排的,有顺序规则。违反了,要么报错,要么 AI 回答乱七八糟。”
规则一:system 必须在第一条。
“有的平台会直接报错,有的不报错但 AI 行为异常。system 就是’开场白’,必须在最前面。”
规则二:user 和 assistant 必须交替出现。
“为什么?”小明问。
“因为大模型是按顺序’读’这个列表的。它看到一个 user 消息,就理解为’用户说了这句话’,然后期望下一条是自己的回复。如果两个 user 连着,它会觉得用户一口气说了两句没给它插话的机会,上下文就乱了。”
“那如果用户连发了两条消息怎么办?”小明追问。
“合并成一条。或者在中间补一个 assistant 消息——比如 '好的,请继续。' 作为过渡。但最干净的做法还是合并。”
规则三:最后一条必须是 user。
“你发给 API 的 messages 列表,最后一条必须是 user 消息。因为你是让 AI 回复你——它需要一个’问题’来回答。如果你最后一条是 assistant,AI 会觉得’我已经回答过了,你还想干嘛?'”
content 的两种形态
老张说:”到目前为止你的 content 都是字符串。但它其实还可以是数组——多模态对话时用。”
“第二种形态里,content 是一个列表,每项有个 type——text 是文字,image_url 是图片。这样 AI 就能同时看到文字和图片。”
“这是多模态对话的基础,后面会专门讲。现在知道 content 不止能放字符串就行。”
消息列表的实战设计模式
老张说:”真正做产品时,messages 列表不是随便攒的,有几种常见的设计模式。”
模式一:分层 System Prompt
“把 system 拆成多条,每条负责一个方面——角色、规则、上下文。比挤在一条里好维护,改哪层动哪层。”
模式二:动态注入上下文
“比如做客服机器人,你可以每轮把’当前订单状态’塞进 system 消息。AI 就能基于最新信息回答,而不是靠用户手动描述。”
模式三:历史编辑
“messages 列表就是一个普通的 Python 列表,你可以增删改查。用户想’撤回’上一句话?删掉。想让 AI’忘掉’某个话题?删掉那几条。灵活得很。”
小明问:”那能不能修改 AI 之前的回复?”
“能。直接改 history 里对应 assistant 消息的 content 就行。但要注意——你改了历史,AI 下一轮就会基于’被篡改的历史’来回答。这在做’对话纠偏’时有用,但别滥用。”
小明点头:”消息列表不只是’存储’,更是’控制’ AI 行为的杠杆。”
“说得好。”老张竖起大拇指。
多轮对话的三个坑
坑一:Token 成本指数增长
老张说:”你每发一轮请求,传的 messages 越来越长。第 1 轮传 2 条,第 10 轮传 20 条,第 50 轮传 100 条。而且每一轮你传的历史里,有 99 条是上一轮已经传过的——重复计费。“
小明倒吸一口气:”那聊久了成本不是飞起?”
“对。假设每条消息平均 50 token,聊 50 轮,最后一轮请求就要传 2500 token 的历史。加上 AI 输出,单次请求就可能消耗三四千 token。”
“所以做聊天产品必须考虑上下文管理——不能无限堆历史。”
坑二:上下文窗口溢出
“每个模型都有上下文长度上限。比如 qwen-plus 是 128K token,deepseek-chat 是 64K,moonshot-v1-8k 光看名字就知道是 8K。”
“你的 history 列表总 token 数超过这个上限,API 直接报错:”
“8K 模型聊个二三十轮可能就炸了。”
解法:
| 策略 | 做法 | 适用场景 |
|---|---|---|
| 固定窗口 | 只保留最近 N 轮对话 | 简单聊天机器人 |
| 滑动窗口 | 按 token 数裁剪,保留最近的 | 通用方案 |
| 摘要压缩 | 定期让 AI 总结历史,用摘要替代原文 | 长对话场景 |
| 混合策略 | 近期保留原文 + 早期用摘要 | 生产级应用 |
老张说:”入门阶段用固定窗口就够了——加一行判断,超过 20 条就删掉最早的对话:”
“system 永远保留,其余只留最近 20 条。简单粗暴,但管用。”
坑三:AI 回复被截断,历史里存了半句话
“这个坑很隐蔽。”老张说,”如果某轮 AI 的回复因为 max_tokens 限制被截断了,你存进 history 的是半句话。下一轮请求时,AI 看到自己上一轮说了半句话,会以为这就是完整的,上下文就乱了。”
解法:检查 finish_reason,如果是 "length" 表示被截断,做个标记:
“至少让你在调试时能发现问题。”
进阶:加点实用的东西
老张说:”基础版跑通了,可以加几个实用功能。”
流式输出——打字机效果
“加 stream=True,API 会一段一段返回,你边收边打印。用户体验从’干等 5 秒看到一大段’变成’像打字一样逐字出现’。ChatGPT 就是这个效果。”
彩色终端输出
“终端里看着舒服多了。Windows 用户可能需要先装 colorama。”
多角色 System Prompt
“可以放多条 system 消息,按优先级排列。复杂的角色设定可以拆成多条,比挤在一条里清晰。”
这篇文章的核心
老张最后在白板上写了一句话:
多轮对话的本质 = 维护一个 messages 列表 + 每轮全部传给 API。
小明看着白板,把它拍了下来。
| 概念 | 一句话 |
|---|---|
| 记忆机制 | 大模型无记忆,靠你传历史消息实现”连续对话” |
| history 列表 | 每轮追加 user 和 assistant 消息,形成完整上下文 |
| 四种角色 | system(人设)、user(提问)、assistant(回复)、tool(函数结果) |
| 消息顺序 | system 必须第一,user/assistant 交替,最后一条必须是 user |
| content 形态 | 纯文本用字符串,多模态用数组(text + image_url) |
| 分层 system | 角色、规则、上下文拆成多条 system,各管各的 |
| 成本问题 | 历史越长 token 越多,需做上下文裁剪 |
| 上下文窗口 | 超过模型上限会报错,入门用固定窗口裁剪即可 |
| 历史编辑 | messages 是普通列表,可增删改查,用于撤回、纠偏、忘话题 |
“大模型不是在’记住’你说过的话,是你在’提醒’它你说过什么。理解了这一点,后面做聊天产品、做 Agent、做 RAG,底层逻辑全是这个。”
下一步
-
上下文管理 — 实现 sliding window 或摘要压缩策略 -
Function Calling — 让 AI 能调用函数,从”聊天”升级到”干活” -
持久化存储 — 把对话历史存到数据库,跨会话保持记忆 -
多模态对话 — 传图片、传文件,不只是文字
“每一步都是在 messages 列表上做加法。底层从来没变过。”
总结
上篇文章你学会了调一次 API。这篇你学会了让 API”记住”对话——核心就是维护一个越来越长的 messages 列表。
40 行代码,一个 while 循环,一个 history 列表,就是一个能连续对话的聊天机器人。但真正让聊天机器人”好用”的,是理解 messages 列表里的门道——四种角色各司其职、消息顺序有硬规则、content 能放文字也能放图片、system 可以分层设计、历史可以增删改查。ChatGPT 的底层也是这个逻辑,只不过它在上面加了流式输出、上下文管理、多模态、插件——但地基就是这个。
小明把这个 CLI 程序跑了一下午,越聊越上瘾。他说:”原来 ChatGPT 也没什么魔法,就是把聊天记录传来传去。”
老张说:”对。大模型开发的所有秘密,都在 messages 这个列表里。“
转载请注明来源:ChatGPT 根本没魔法!一个 while 循环 + 一个列表,我做出了聊天机器人

























