ChatGPT 根本没魔法!一个 while 循环 + 一个列表,我做出了聊天机器人

    |     2026年8月16日   |   agent技术, AI大模型应用   |     0 条评论   |    8

引言

小明上周转通了第一次 API 调用,兴奋了没两天就发现一个问题——

他问 AI:”我有十个苹果?吃掉一个还有几个?”AI 回答了。他接着问:”我原来有几个苹果”AI 回:”不知道原来苹果数?请补充你的需求。”

小明愣了。这不是接着上一句问的吗?

他去找老张:”这 AI 是不是有健忘症?”

老张笑了:”不是健忘症,是根本没有记忆。你每次调 API,对它来说都是全新的一次对话。它不知道你上一句问了什么。”

“那 ChatGPT 怎么能连续聊?”

“因为 ChatGPT 每次都把之前聊过的所有消息打包传给大模型。你以为它在’记住’,其实是它在’复读’——把历史消息全部重新喂一遍。”

小明恍然大悟。老张说:”今天我就带你实现一个能连续对话的命令行聊天机器人。核心就一句话——维护一个 messages 列表,每轮把历史消息全部传过去。

大模型的”记忆”从哪来

老张先在白板上画了张图:

第1轮请求:
你发 → [system, user1]
AI回 ← assistant1

第2轮请求:
你发 → [system, user1, assistant1, user2]
AI回 ← assistant2

第3轮请求:
你发 → [system, user1, assistant1, user2, assistant2, user3]
AI回 ← assistant3

“看到了没?”老张说,”每一轮对话,你发给 API 的 messages 列表越来越长。大模型看到完整的历史,就能接上下文。这个列表就是你这个聊天机器人的’记忆’。”

小明问:”那这个列表一直涨,不会爆吗?”

“会。”老张说,”这是多轮对话最核心的问题,待会儿讲。先把基础版跑通。”

多轮对话核心代码

老张说:”配置和上次一样,看核心文件。这次就一个文件,不到 40 行。”

"""
第4课:多轮对话 CLI(messages 历史累积)。

运行:python main.py
输入 quit 退出。
"""

from config import MODEL, get_client


def main() -> None:
  client = get_client()
  history = [{"role""system""content""你是一位简洁的助理。"}]

  print("数字员工已就绪。输入 quit 退出。\n")

  while True:
    user_input = input("你:").strip()
    if not user_input or user_input.lower() in ("quit""exit""/quit"):
      print("再见!")
      break

    history.append({"role""user""content": user_input})
    response = client.chat.completions.create(
      model=MODEL,
      messages=history,
      temperature=0.7,
    )
    assistant = response.choices[0].message.content or ""
    history.append({"role""assistant""content": assistant})
    print(f"AI:{assistant}\n")


if __name__ == "__main__":
  main()

老张说:”别看代码短,每一行都有讲究。我逐段给你讲。”

逐段拆解

第一步:初始化历史列表

history = [{"role""system""content""你是一位简洁的助理。"}]

“这个 history 列表就是整个对话的’记忆容器’。程序启动时只有一条 system 消息——给 AI 定人设。”

“这里写的是’你是一位简洁的助理’,意思是让 AI 回答简短点,别长篇大论。你可以改成任何你想要的角色——’你是一个 Python 编程专家”你是一个温柔的心理咨询师’,都行。”

小明问:”这个 system 消息每轮都要传?”

“对。它放在 history[0],每轮请求都会跟着一起发过去。相当于每次都提醒 AI ‘你是谁’。”

第二步:循环接收输入

while True:
  user_input = input("你:").strip()
  if not user_input or user_input.lower() in ("quit""exit""/quit"):
    print("再见!")
    break

while True 死循环,不断等用户输入。input("你:") 在终端显示 你: 提示符,等用户敲回车。”

“退出逻辑三道防线——输入 quitexit/quit 都能退出,大小写不敏感。输入空行也直接跳过,不会发空消息给 API 浪费 token。”

第三步:累积消息,发起请求

history.append({"role""user""content": user_input})
response = client.chat.completions.create(
  model=MODEL,
  messages=history,
  temperature=0.7,
)

“这是核心中的核心。”老张加重了语气,”先把用户的话追加到 history,然后把整个 history 传给 API。”

“注意,传的是 messages=history,不是 messages=[{"role": "user", "content": user_input}]。这就是多轮对话和单轮对话唯一的区别——你传给 API 的消息列表包含了所有历史。

第四步:保存 AI 回复,形成闭环

assistant = response.choices[0].message.content or ""
history.append({"role""assistant""content": assistant})
print(f"AI:{assistant}\n")

“拿到 AI 的回复后,必须追加到 history。这一步很多人会忘——只打印了回复,没存进历史。结果下一轮请求时,AI 不知道自己上一轮说了什么,上下文就断了。”

or "" 是防御性写法——万一 API 返回 None(极端情况),不会报错。”

老张在白板上画了个闭环:

用户输入 → 追加到 history → 调 API → AI 回复 → 追加到 history → 打印 → 下一轮

“四步循环,就是这个 while 循环的全部。”

跑起来看看

小明运行了程序:

python main.py
数字员工已就绪。输入 quit 退出。

你:我叫小明,今年25岁
AI:你好,小明!25岁正是黄金年龄。

你:我刚才说我叫什么?
AI:你叫小明。

你:我多大了?
AI:你今年25岁。

你:quit
再见!

小明瞪大眼睛:”它记住了!第二三轮问它都知道!”

“不是它记住了,是你每次都告诉它了。”老张说,”你在第三轮问’我多大了’的时候,你发给 API 的 messages 列表长这样:”

[
  {"role""system""content""你是一位简洁的助理。"},
  {"role""user""content""我叫小明,今年25岁"},
  {"role""assistant""content""你好,小明!25岁正是黄金年龄。"},
  {"role""user""content""我刚才说我叫什么?"},
  {"role""assistant""content""你叫小明。"},
  {"role""user""content""我多大了?"},   # ← 当前问题
]

“它看到第一轮你就说了’今年 25 岁’,当然知道你多大。记忆不是大模型的能力,是你把历史喂给它的结果。

小明若有所思:”那如果聊了一百轮,这个列表岂不是有一百多条?”

“对,而且每条越来越长。这就是接下来要说的问题。”

深入理解消息列表

老张喝了口水,说:”基础用法你清楚了,但 messages 列表远不止’攒一堆消息传过去’这么简单。这里面的细节,决定了你的聊天机器人到底是’能聊’还是’好聊’。”

消息的解剖结构

“先看一条消息长什么样。你平时写的是这样:”

{"role""user""content""你好"}

“但完整的一条消息,其实可以包含更多字段:”

{
  "role""user",       # 必须:谁说的
  "content""你好",     # 必须:说了什么
  "name""小明"       # 可选:说话人的名字
}

老张说:”rolecontent 是必填的,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 必须在第一条。

# ✅ 正确
[
  {"role""system""content""你是助理"},
  {"role""user""content""你好"},
]

# ❌ 错误——system 放后面
[
  {"role""user""content""你好"},
  {"role""system""content""你是助理"},
]

“有的平台会直接报错,有的不报错但 AI 行为异常。system 就是’开场白’,必须在最前面。”

规则二:user 和 assistant 必须交替出现。

# ✅ 正确——交替排列
[
  {"role""system""content""你是助理"},
  {"role""user""content""问题1"},
  {"role""assistant""content""回答1"},
  {"role""user""content""问题2"},
]

# ❌ 错误——两个 user 连着
[
  {"role""system""content""你是助理"},
  {"role""user""content""问题1"},
  {"role""user""content""问题2"},
]

“为什么?”小明问。

“因为大模型是按顺序’读’这个列表的。它看到一个 user 消息,就理解为’用户说了这句话’,然后期望下一条是自己的回复。如果两个 user 连着,它会觉得用户一口气说了两句没给它插话的机会,上下文就乱了。”

“那如果用户连发了两条消息怎么办?”小明追问。

“合并成一条。或者在中间补一个 assistant 消息——比如 '好的,请继续。' 作为过渡。但最干净的做法还是合并。”

# 用户连发两条 → 合并
user_input = "问题1 问题2"
history.append({"role""user""content": user_input})

规则三:最后一条必须是 user。

“你发给 API 的 messages 列表,最后一条必须是 user 消息。因为你是让 AI 回复你——它需要一个’问题’来回答。如果你最后一条是 assistant,AI 会觉得’我已经回答过了,你还想干嘛?'”

# ✅ 最后一条是 user——AI 知道该回答了
[
  {"role""system""content""你是助理"},
  {"role""user""content""你好"},
  {"role""assistant""content""你好!"},
  {"role""user""content""今天天气怎么样?"},  # ← 最后一条
]

# ❌ 最后一条是 assistant——AI 困惑
[
  {"role""system""content""你是助理"},
  {"role""user""content""你好"},
  {"role""assistant""content""你好!"},  # ← 最后一条,AI 不知道要干嘛
]

content 的两种形态

老张说:”到目前为止你的 content 都是字符串。但它其实还可以是数组——多模态对话时用。”

# 形态一:纯文本(你一直在用的)
{"role""user""content""这张图是什么?"}

# 形态二:多模态数组(图文混合)
{"role""user""content": [
  {"type""text""text""这张图是什么?"},
  {"type""image_url""image_url": {"url""https://example.com/cat.jpg"}},
]}

“第二种形态里,content 是一个列表,每项有个 type——text 是文字,image_url 是图片。这样 AI 就能同时看到文字和图片。”

“这是多模态对话的基础,后面会专门讲。现在知道 content 不止能放字符串就行。”

消息列表的实战设计模式

老张说:”真正做产品时,messages 列表不是随便攒的,有几种常见的设计模式。”

模式一:分层 System Prompt

history = [
  # 第一层:全局角色
  {"role""system""content""你是'小助',一个技术问答助手。"},
  # 第二层:行为规则
  {"role""system""content""回答要简洁,附带代码示例。不要编造不存在的API。"},
  # 第三层:上下文信息
  {"role""system""content""用户使用的是Python 3.12,操作系统是Windows 11。"},
]

“把 system 拆成多条,每条负责一个方面——角色、规则、上下文。比挤在一条里好维护,改哪层动哪层。”

模式二:动态注入上下文

# 每轮对话前,把最新信息注入 system
def build_messages(history, user_input, context_info):
  # 替换或追加上下文 system 消息
  history[1] = {"role""system""content"f"当前时间:{context_info}"}
  history.append({"role""user""content": user_input})
  return history

“比如做客服机器人,你可以每轮把’当前订单状态’塞进 system 消息。AI 就能基于最新信息回答,而不是靠用户手动描述。”

模式三:历史编辑

# 用户说"忘了我刚才说的",删除最后一条 user 消息
if user_input == "忘了我刚才说的":
  history.pop()  # 删掉刚追加的 user 消息
  continue

“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 直接报错:”

Error: This model's maximum context length is 8192 tokens. However, your messages resulted in 10234 tokens.

“8K 模型聊个二三十轮可能就炸了。”

解法

策略 做法 适用场景
固定窗口 只保留最近 N 轮对话 简单聊天机器人
滑动窗口 按 token 数裁剪,保留最近的 通用方案
摘要压缩 定期让 AI 总结历史,用摘要替代原文 长对话场景
混合策略 近期保留原文 + 早期用摘要 生产级应用

老张说:”入门阶段用固定窗口就够了——加一行判断,超过 20 条就删掉最早的对话:”

# 保留 system + 最近 20 条
if len(history) > 21:
  history = [history[0]] + history[-20:]

“system 永远保留,其余只留最近 20 条。简单粗暴,但管用。”

坑三:AI 回复被截断,历史里存了半句话

“这个坑很隐蔽。”老张说,”如果某轮 AI 的回复因为 max_tokens 限制被截断了,你存进 history 的是半句话。下一轮请求时,AI 看到自己上一轮说了半句话,会以为这就是完整的,上下文就乱了。”

解法:检查 finish_reason,如果是 "length" 表示被截断,做个标记:

response = client.chat.completions.create(
  model=MODEL,
  messages=history,
  temperature=0.7,
)
choice = response.choices[0]
assistant = choice.message.content or ""

# 如果被截断,追加一个提示
if choice.finish_reason == "length":
  assistant += "(回复被截断)"

history.append({"role""assistant""content": assistant})

“至少让你在调试时能发现问题。”

进阶:加点实用的东西

老张说:”基础版跑通了,可以加几个实用功能。”

流式输出——打字机效果

stream = client.chat.completions.create(
  model=MODEL,
  messages=history,
  temperature=0.7,
  stream=True,  # 加这一个参数
)
assistant = ""
for chunk in stream:
  delta = chunk.choices[0].delta.content
  if delta:
    print(delta, end="", flush=True)
    assistant += delta
print()  # 换行

“加 stream=True,API 会一段一段返回,你边收边打印。用户体验从’干等 5 秒看到一大段’变成’像打字一样逐字出现’。ChatGPT 就是这个效果。”

彩色终端输出

# 用户输入用青色,AI回复用绿色
USER_COLOR = "\033[36m"
AI_COLOR = "\033[32m"
RESET = "\033[0m"

print(f"{USER_COLOR}你:{RESET}", end="")
# ...
print(f"{AI_COLOR}AI:{assistant}{RESET}\n")

“终端里看着舒服多了。Windows 用户可能需要先装 colorama。”

多角色 System Prompt

history = [
  {"role""system""content""你是一位资深Python工程师,回答时附带代码示例。"},
  {"role""system""content""如果用户的问题不涉及编程,引导话题回到技术领域。"},
]

“可以放多条 system 消息,按优先级排列。复杂的角色设定可以拆成多条,比挤在一条里清晰。”

这篇文章的核心

老张最后在白板上写了一句话:

多轮对话的本质 = 维护一个 messages 列表 + 每轮全部传给 API。

小明看着白板,把它拍了下来。

概念 一句话
记忆机制 大模型无记忆,靠你传历史消息实现”连续对话”
history 列表 每轮追加 user 和 assistant 消息,形成完整上下文
四种角色 system(人设)、user(提问)、assistant(回复)、tool(函数结果)
消息顺序 system 必须第一,user/assistant 交替,最后一条必须是 user
content 形态 纯文本用字符串,多模态用数组(text + image_url)
分层 system 角色、规则、上下文拆成多条 system,各管各的
成本问题 历史越长 token 越多,需做上下文裁剪
上下文窗口 超过模型上限会报错,入门用固定窗口裁剪即可
历史编辑 messages 是普通列表,可增删改查,用于撤回、纠偏、忘话题

“大模型不是在’记住’你说过的话,是你在’提醒’它你说过什么。理解了这一点,后面做聊天产品、做 Agent、做 RAG,底层逻辑全是这个。”

下一步

  1. 上下文管理 — 实现 sliding window 或摘要压缩策略
  2. Function Calling — 让 AI 能调用函数,从”聊天”升级到”干活”
  3. 持久化存储 — 把对话历史存到数据库,跨会话保持记忆
  4. 多模态对话 — 传图片、传文件,不只是文字

“每一步都是在 messages 列表上做加法。底层从来没变过。”

总结

上篇文章你学会了调一次 API。这篇你学会了让 API”记住”对话——核心就是维护一个越来越长的 messages 列表

40 行代码,一个 while 循环,一个 history 列表,就是一个能连续对话的聊天机器人。但真正让聊天机器人”好用”的,是理解 messages 列表里的门道——四种角色各司其职、消息顺序有硬规则、content 能放文字也能放图片、system 可以分层设计、历史可以增删改查。ChatGPT 的底层也是这个逻辑,只不过它在上面加了流式输出、上下文管理、多模态、插件——但地基就是这个。

小明把这个 CLI 程序跑了一下午,越聊越上瘾。他说:”原来 ChatGPT 也没什么魔法,就是把聊天记录传来传去。”

老张说:”对。大模型开发的所有秘密,都在 messages 这个列表里。

转载请注明来源:ChatGPT 根本没魔法!一个 while 循环 + 一个列表,我做出了聊天机器人
本文链接地址:https://ai.zhousir.top/?p=3605
回复 取消