这一节要做什么

在第1节的代码基础上,只增加一个功能:把对话存起来,每次发送给大模型时带上完整历史。


第1节→第2节的差异

第1节的问题是:每次发消息,LLM只看到"当前这句话",不记得之前说过什么。

第1节的对话:
  第1轮:你说"你好"
  第2轮:LLM忘了你说过的"你好",只能看到"今天天气怎么样"
  (LLM不知道上一轮说了"你好")

第2节加了一个"记忆本"(Python 列表),每次对话结束后把说的话记下来,下次发消息时把整本记忆本一起发过去。

第2节的对话:
  第1轮:你说"你好"  →  记忆本:[用户:你好]
                      →  LLM看到:[用户:你好]  → 回复"你好!"
  第2轮:LLM看到:记忆本:[用户:你好, 助手:你好!, 用户:今天天气怎么样]
                      →  记住了之前的"你好"

这一节只多了一样东西:一个可以追加内容的 messages 列表。


文件结构:两个文件

本节有两个文件,先看清楚它们的关系:

文件

是什么

职责

第2节-chat2_base.py

底层接口层

从第1节继承 url/api_key/model,包装成 send() 函数

第2节-chat2.py

上层调用层

定义 messages 记忆本 + chat() 函数,调用 base.send()

第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.pychat() 复用 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 里的函数,它内部做了这些事(你不需要再写一遍):

  1. 把 messages 包装成 HTTP 请求

  2. 发给大模型 API

  3. 等待回答

  4. 从返回结果里提取出回答文本

  5. 返回这个文本

result 是什么:
大模型的回答,是一个字符串。比如 "你好!有什么可以帮助你的吗?"

闭包原理:
base.py 里的 send() 函数用到了 urlapi_keymodel 这些变量,但 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"
这样用户输入 EXITExitexit 都能识别。

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 是一个无限循环,程序会一直运行,直到遇到 break

  • input("\n你: ") 每次循环都停下来等你输入,你按回车后继续

  • break 跳出循环,程序结束


运行效果

运行 python 第2节-chat2.py,屏幕显示:

==================================================
第2节:记忆(短期)——交互式对话
输入 exit / quit / 退出 结束程序
==================================================

你: 你好
AI: 你好!有什么我可以帮助你的吗?
你: 你还记得我刚才说了什么吗
AI: 你刚才说的是"你好",然后问我是否记得你说过的话。
你: 今天天气真好
AI: 是的,今天天气确实不错。有什么想聊的吗?
你: 退出
再见!

这个设计的问题(承上启下)

问题: 记忆本存在内存里,程序一关就没了。下次运行,是一个全新的空记忆本。

解决: 第3节会解决这个问题——把记忆本写到文件里(HISTORY.md),程序重启后能读取之前的内容。这就是"长期记忆"。


这一节学到了什么

概念

一句话解释

短期记忆

存在内存里的对话历史,程序关闭就消失

messages 列表

记忆本的结构——每项是 {"role": "xxx", "content": "xxx"}

完整历史发送

每次请求发送所有对话,LLM就"以为"自己有记忆

while True + input()

让程序停下来等你输入,实现交互式对话

sys.path.insert

把当前文件夹加入 Python 搜索路径,这样 Python 才能找到同目录下的 py 文件


这一节的代码对应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隔离等)。