这一节要做什么
在第1节的代码基础上,只增加一个功能:把对话存起来,每次发送给大模型时带上完整历史。
第1节→第2节的差异
第1节的问题是:每次发消息,LLM只看到"当前这句话",不记得之前说过什么。
第1节的对话:
第1轮:你说"你好"
第2轮:LLM忘了你说过的"你好",只能看到"今天天气怎么样"
(LLM不知道上一轮说了"你好")
第2节加了一个"记忆本"(Python 列表),每次对话结束后把说的话记下来,下次发消息时把整本记忆本一起发过去。
第2节的对话:
第1轮:你说"你好" → 记忆本:[用户:你好]
→ LLM看到:[用户:你好] → 回复"你好!"
第2轮:LLM看到:记忆本:[用户:你好, 助手:你好!, 用户:今天天气怎么样]
→ 记住了之前的"你好"
这一节只多了一样东西:一个可以追加内容的 messages 列表。
文件结构:两个文件
本节有两个文件,先看清楚它们的关系:
第1节-chat1.py ← 第1节的原始代码(url/api_key/model + requests.post 写在一起)
↓ 复制 + 函数封装
第2节-chat2_base.py ← 第1节拆分出来:url/api_key/model + send() 函数,专注"发请求"
↓ 调用
第2节-chat2.py ← 第2节新增:messages 记忆本 + chat() 函数,专注"管理记忆"
传承关系:
base.send()不打印任何内容,只负责把 messages 发给大模型、返回回答chat2.py的chat()复用base.send(),自己只专注记忆本的管理(追加用户的话 → 发请求 → 追加AI回复)两层各做各的事,不重复
文件一:第2节-chat2_base.py(底层,只保留发请求的逻辑)
# ============================================================
# 第2节底层:把第1节的代码原封不动复制过来
# 只做两件事:① 去掉 print 语句 ② 包装成 send() 函数
# 除此之外,一个字都不改
# ============================================================
# ---------- 1. 导入工具 ----------
import requests
# ---------- 2. 设置大模型的"地址" ----------
url = "https://api.siliconflow.cn/v1/chat/completions"
api_key = "sk-xxxxxxxxxxxxxxxxxxxxxxxx"
model = "deepseek-ai/DeepSeek-V3.2"
def send(messages: list) -> str:
"""
第1节的逻辑:发一个问题给大模型,拿一个回答。
原封不动包装成函数,只去掉 print,不动任何其他逻辑。
"""
# ---------- 3. 准备要发给大模型的数据 ----------
# (第1节这里是写死的 messages = [{"role": "user", "content": "你好"}]
# 第2节把 messages 作为参数传进来,所以这里不再写死)
# ---------- 4. 拼装完整的HTTP请求 ----------
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}"
}
body = {
"model": model,
"messages": messages
}
# ---------- 5. 发送请求,等待大模型回复 ----------
response = requests.post(url, headers=headers, json=body, timeout=60)
# ---------- 6. 从返回结果里提取大模型的回答 ----------
result = response.json()["choices"][0]["message"]["content"]
# ---------- 7. 打印结果 ----------
# (去掉了第1节的 print(result),调用者自己决定怎么用)
return result
解释:
这个文件只做一件事:把 messages 发给大模型、返回回答
没有任何 print 语句——调用者(chat2.py)自己决定怎么用这个回答
第1节继承来的
url/api_key/model都在这里,send()函数通过闭包引用它们
对比: 第1节是"发一条 → 打印 → 结束",第2节底层是"包装成函数 → 等调用者来用"。
文件二:第2节-chat2.py(上层调用层,专注记忆本管理)
本节的核心思路:url/api_key/model 和发请求的逻辑全部放到 base.py 里,chat2.py 只管两件事:维护记忆本、调用
base.send()。
# ---------- 1. 导入工具 ----------
# 这两行是 Python 的"文件夹路径"工具
import os
import sys
# ---------- 2. 为什么要这两行? ----------
# Python 默认只会去"标准地方"找模块,找不到我们自己写的 py 文件
# 这两行的意思是:把"当前这个文件所在的文件夹"也加入搜索路径
# 这样 Python 就能找到同目录下的 chat2_base.py 了
# os.path.abspath(__file__) = 获取当前文件的完整路径
# os.path.dirname(...) = 取出这个文件所在的文件夹路径
# sys.path.insert(0, ...) = 把这个文件夹路径加入到 Python 的模块搜索列表的最前面
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
# ---------- 3. 导入 chat2_base.py 里的 send 函数 ----------
from chat2_base import send
# ---------- 4. 初始化"记忆本" ----------
# 记忆本是一个空列表,每次对话(用户说 + AI回复)都追加进去,一直保留
messages = [] # 空的记忆本,用来存放所有对话记录(用户说过的 + AI回复过的)
# ---------- 5. 定义一个"聊天函数" ----------
# 包装成 chat() 函数:用户说一句话 → 记到记忆本 → 发给LLM → 把LLM回复也记到记忆本 → 返回回答
def chat(user_input: str) -> str:
"""
发送一条消息给大模型,返回大模型的回答。
每次调用都会自动把对话记到 messages 列表里。
"""
# 5a. 把用户的话追加到记忆本
messages.append({
"role": "user",
"content": user_input
})
# 5b. 调用 base.send() 把完整记忆本发给LLM,拿回答
result = send(messages)
# 5c. 把LLM的回复也追加到记忆本
messages.append({
"role": "assistant",
"content": result
})
return result
# ---------- 6. 开始交互式对话 ----------
# 程序进入无限循环:停在这里等你输入,你输入一句话就调用 chat() 回答你,直到你说"退出"
print("=" * 50)
print("第2节:记忆(短期)——交互式对话")
print("输入 exit / quit / 退出 结束程序")
print("=" * 50)
while True:
user_input = input("\n你: ")
if user_input.lower() in ("exit", "quit", "退出"):
print("再见!")
break
reply = chat(user_input)
print(f"AI: {reply}")
chat2.py 逐行详解
导入部分:为什么要 sys.path.insert(0, ...)?
Python 导入模块时,会去一个"搜索路径列表"里找。
Python 默认的搜索路径有这些:
1. 当前运行的 .py 文件所在目录
2. Python 安装目录里的 lib 文件夹
3. 一些标准库位置
问题来了:
第1节的
import requests为什么能直接用?因为requests是安装好的第三方库,Python 在"标准地方"能找到但我们自己写的
chat2_base.py不在标准地方,Python 找不到
解决方案:手动把当前文件夹加进去
import os
import sys
# 一步步拆开看:
# __file__ = "第2节-chat2.py"(当前这个文件的文件名)
# os.path.abspath(__file__) = "e:\项目\...\第2节-chat2.py"(完整路径)
# os.path.dirname(...) = "e:\项目\...\"(文件夹路径)
# sys.path = Python 用来搜索模块的路径列表
# sys.path.insert(0, ...) = 把我们想要的文件夹路径插到列表最前面
# 插入到最前面是为了:万一有重名的模块,优先用我们这个文件夹里的
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
效果:
加之前:Python 只知道去"标准地方"找
加之后:Python 知道也去"当前文件夹"找
然后就能正常导入了:
from chat2_base import send # 找到了!Python 知道去当前文件夹找
第二行:messages = []
这一行做了什么:
创建一个空的 Python 列表,叫 messages。这整个列表就是"记忆本"。
什么是记忆本:messages 是一个列表,里面每一项是一轮对话,格式是 {"role": "角色", "content": "说的话"}。
role: "user"= 人类说的话role: "assistant"= AI说的话
为什么是空的 []:
第1节写的是 messages = [{"role": "user", "content": "你好"}],一开始就有内容。
第2节的记忆本一开始是空的,随着对话进行,一点一点往里加内容。这样每次发给 LLM 的都是"所有对话历史",LLM 就能"记住"之前说过什么了。
记忆本的样子(随着对话进行会变成这样):
messages = [
{"role": "user", "content": "你好"}, # 第1轮:人类
{"role": "assistant", "content": "你好!"}, # 第1轮:AI
{"role": "user", "content": "今天天气不错"}, # 第2轮:人类
{"role": "assistant", "content": "是啊..."}, # 第2轮:AI
]
第三行:def chat(user_input: str) -> str:
这一行做了什么:
定义了一个函数,名字叫 chat,接收一个参数 user_input(字符串类型),返回一个字符串。
什么是函数:
函数就像一个"自动贩卖机":
你投入一个
user_input(你说的话)贩卖机内部自动完成:记到记忆本 → 发给大模型 → 等回复 → 把回复返回给你
你只需要调用
chat("你好"),不用管里面怎么运作的
参数 user_input: str 是什么意思:user_input 是你(人类)说的话,比如"你好"、"今天天气怎么样"。: str 是类型标注,意思是"这是一个字符串",只是说明,不影响运行。
返回值 -> str 是什么意思:
函数运行完成后会"吐出"一个字符串,就是大模型的回答。
5a步:messages.append({"role": "user", "content": user_input})
这行做了什么:
把人类说的话追加到记忆本里。
什么是 append:append 是列表的"往最后加一项"操作。messages.append(x) = 把 x 加到 messages 的最后面。
执行前:
messages = [] # 空的
执行后:
messages = [{"role": "user", "content": "你好"}] # 有一项了
这条命令的目的:
把用户说的话记下来,这样才能在下一轮对话时一起发给 LLM。记忆本里没有记过的东西,LLM 是不知道的。
5b步:result = send(messages)
这行做了什么:
调用 base.py 里定义好的 send() 函数,把整个记忆本 messages 发给大模型,收到回答后存到 result 里。
什么是 send():send() 是 base.py 里的函数,它内部做了这些事(你不需要再写一遍):
把 messages 包装成 HTTP 请求
发给大模型 API
等待回答
从返回结果里提取出回答文本
返回这个文本
result 是什么:
大模型的回答,是一个字符串。比如 "你好!有什么可以帮助你的吗?"
闭包原理:
base.py 里的 send() 函数用到了 url、api_key、model 这些变量,但 chat2.py 里并没有定义这些变量。
这是因为 send() 通过"闭包"引用了 base.py 里定义的 url/api_key/model。就像 send() 记住了自己出生时的环境,chat2.py 调用它时,它还是能找到那些配置。
5c步:messages.append({"role": "assistant", "content": result})
这行做了什么:
把 AI 的回答也追加到记忆本里。
执行后:
messages = [
{"role": "user", "content": "你好"},
{"role": "assistant", "content": "你好!有什么可以帮助你的吗?"}
]
为什么 AI 的回复也要记:
因为下一轮发给 LLM 时,messages 里要包含"AI 自己说过的话"。LLM 需要知道自己是谁、说过什么,才能保持一致。
如果不记会怎样:
第1轮你说了"你好",AI 回答"你好!"。第2轮你说"我叫小明"。
如果记忆本里没有 AI 第1轮的回复,LLM 就不知道第1轮自己说过"你好!",可能出现"精神分裂"式的回复——它不记得自己是中文还是英文、有没有自我介绍过了。
return result
这行做了什么:
把大模型的回答"吐出来",返回给调用者(就是 while 循环里的 reply = chat(user_input) 这一行)。
return 的意义:result 是函数内部的变量,函数结束就消失了。用 return result 把结果交给调用者,这样调用者才能用这个回答(比如 print(f"AI: {reply}"))。
while True: 循环开始
print(...) 做了什么:
在屏幕上打印提示文字,告诉用户这个程序是做什么的、怎么退出。
while True: 是什么
这行做了什么:
开始一个无限循环。程序会一直停在这里,等你输入、回复、再输入……直到遇到 break。
while True 的意思:
while= "当……的时候"True= "真的"合起来 = "当条件为真的时候,一直做下面的事"
while True没有退出条件,所以是"无限循环"
user_input = input("\n你: ")
这行做了什么:input() 是 Python 的"等待用户输入"命令。
程序运行到这里会停下来,等你输入一句话,按回车。
你输入的内容会变成字符串,存进 user_input 变量。
\n 是什么:
换行符。打印出来就是换到下一行。所以屏幕上显示的是:
你: [光标在这里等你输入]
if user_input.lower() in ("exit", "quit", "退出"):
这行做了什么:
判断用户是否想退出。
.lower() 是什么:
把英文字母全部变成小写。比如 "EXIT".lower() = "exit"。
这样用户输入 EXIT、Exit、exit 都能识别。
in ("exit", "quit", "退出") 是什么:
判断输入是不是在"退出名单"里。只要输入是 exit / quit / 退出 其中的任何一个,就执行下面的 break。
break
这行做了什么:
跳出 while True 循环,程序结束。
没有 break 会怎样:
程序会永远运行下去,停不下来——因为 while True 不会自动停,除非遇到 break。
reply = chat(user_input)
这行做了什么:
把你输入的 user_input 交给 chat() 函数,收到 AI 的回答后存到 reply 里。
reply 是什么:
就是 AI 的回答,一个字符串。
print(f"AI: {reply}")
这行做了什么:
把 AI 的回答打印到屏幕上。
f"AI: {reply}" 是什么:
f-string(格式化字符串)。{} 里的变量会被替换成实际的值。
比如 reply = "你好!" → 打印出来就是 AI: 你好!
逐行详解:第2节新增了什么
差异1:记忆本初始化
# 第1节的记忆:写死在代码里,发完就丢了
messages = [
{
"role": "user",
"content": "你好,介绍一下你自己"
}
]
# 第2节的记忆:空列表,每次对话追加进去,一直保留
messages = []
类比: 第1节像每次对话拿一张新纸,写完就扔;第2节换成一本笔记本,写完继续用,下一页加新内容。
差异2:发送完整记忆本(最核心的变化)
# 第1节:每次只发当前这一句话
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"}
body = {"model": model, "messages": messages} # messages 里只有当前这一句
response = requests.post(url, headers=headers, json=body, timeout=60)
result = response.json()["choices"][0]["message"]["content"]
print(result)
# 第2节:一行调用 base.send(messages),把完整记忆本发过去
result = send(messages) # ← 就这一行,发请求+提取回答,全部在 base.send() 里完成
这是第2节最核心的增量: LLM没有记忆,每次都要靠发送的 messages 列表来"假装"有记忆。记录双方才能让LLM理解对话的来龙去脉。
差异3:大模型回复也要记到记忆本
# 这一步是新加的:把LLM的回复追加到记忆本
messages.append({
"role": "assistant",
"content": result
})
为什么AI的回复也要记? 因为下一轮发给LLM的 messages 里要包含"AI自己说过的话",LLM才能知道它自己是谁、说过什么。
差异4:while True 循环(交互式的核心)
# while True:程序停在这里等你输入,你输入一句就调用 chat() 回复你,直到你说"退出"
while True:
user_input = input("\n你: ")
if user_input.lower() in ("exit", "quit", "退出"):
print("再见!")
break
reply = chat(user_input)
print(f"AI: {reply}")
解释:
while True是一个无限循环,程序会一直运行,直到遇到breakinput("\n你: ")每次循环都停下来等你输入,你按回车后继续break跳出循环,程序结束
运行效果
运行 python 第2节-chat2.py,屏幕显示:
==================================================
第2节:记忆(短期)——交互式对话
输入 exit / quit / 退出 结束程序
==================================================
你: 你好
AI: 你好!有什么我可以帮助你的吗?
你: 你还记得我刚才说了什么吗
AI: 你刚才说的是"你好",然后问我是否记得你说过的话。
你: 今天天气真好
AI: 是的,今天天气确实不错。有什么想聊的吗?
你: 退出
再见!
这个设计的问题(承上启下)
问题: 记忆本存在内存里,程序一关就没了。下次运行,是一个全新的空记忆本。
解决: 第3节会解决这个问题——把记忆本写到文件里(HISTORY.md),程序重启后能读取之前的内容。这就是"长期记忆"。
这一节学到了什么
这一节的代码对应nanobot的哪里
用户发消息
↓
本节新增:messages.append({"role": "user", ...}) ← 把用户的话记到记忆本
↓
本节新增:messages 列表发送给大模型 ← 发送完整历史,不是单条
↓
大模型回复后:本节新增:messages.append({"role": "assistant", ...}) ← 把LLM回复也记下来
↓
nanobot 对应位置:session/manager.py 里的 Session.messages(内存列表)
nanobot 里用 session.add_message(role, content) 往记忆本里追加内容,和本节的 messages.append() 是一样的逻辑,只是包装得更完善(加了文件持久化、多session隔离等)。