手写多轮对话循环

现在用第 2 章的 LLMClient 写一个真正的对话程序:在终端里跟模型连续聊天,模型能「记住」前面说过的话。多轮对话的全部秘密就一句话——把每次来往的消息都存进同一个 messages 列表,每次请求把整个列表发给模型。本章的 main.py 完成后,第 6~8 章的智能体循环都以它为雏形。

1. 为什么模型会「失忆」

模型的每次调用都是独立的:它看不到上一次调用的内容,只看到你这次传进去的 messages。所以:

messages = [{"role": "user", "content": "我叫小明,喜欢打篮球"}]
# ...把上面这条发给模型,得到回复
# 下一次请求若只发「我叫什么名字?」,模型无从得知

正确做法是每次把「用户说过的话 + 助手说过的话」全部带上:

messages = [
    {"role": "user", "content": "我叫小明,喜欢打篮球"},
    {"role": "assistant", "content": "你好小明!"},
    {"role": "user", "content": "我叫什么名字?"},
]

2. 完整代码 main.py

"""main.py —— 第 3 章:手写多轮对话循环(在终端里连续聊天)。"""
from typing import List

from llm_client import LLMClient

SYSTEM_PROMPT = "你是一个乐于助人的助手,回答请简洁、使用简体中文。"

EXIT_WORDS = {"/exit", "/quit", "退出", "再见"}
CLEAR_WORDS = {"/clear", "/reset", "重置", "清空"}


def main() -> None:
    llm = LLMClient()
    messages: List[dict] = [{"role": "system", "content": SYSTEM_PROMPT}]
    print("对话开始。/exit 退出;/clear 清空上下文。")

    while True:
        try:
            user_input = input("你:").strip()
        except (KeyboardInterrupt, EOFError):   # Ctrl+C / Ctrl+D 优雅退出
            print("\n再见!")
            break

        if not user_input:
            continue
        if user_input in EXIT_WORDS:
            print("再见!")
            break
        if user_input in CLEAR_WORDS:
            messages = [{"role": "system", "content": SYSTEM_PROMPT}]  # 只留系统提示
            print("(上下文已清空)")
            continue

        messages.append({"role": "user", "content": user_input})
        answer = llm.chat_text(messages)
        messages.append({"role": "assistant", "content": answer})
        print(f"助手:{answer}")


if __name__ == "__main__":
    main()

3. 运行与对话轨迹

python main.py

一次真实运行的对话轨迹大致如下(回复内容随模型与时间变化):

对话开始。/exit 退出;/clear 清空上下文。
你:我叫小明,喜欢打篮球
助手:你好小明!篮球是一项很棒的运动,你平时打什么位置?
你:我叫什么名字?我喜欢什么运动?
助手:你叫小明,喜欢打篮球。——这就是“上下文记忆”,模型靠的是 messages 列表而非记忆
你:/clear
(上下文已清空)
你:我叫什么名字?
助手:抱歉,我们还没有聊过这个话题,方便告诉我你的名字吗?

注意最后一步:清空上下文后模型「忘了」小明,正好印证第 1 节的说法——记忆全部来自 messages。

4. 要点拆解

  • 消息角色只有三种:system(设定人设/规则,放最前)、user(用户)、assistant(模型回复)。聊天记录里必须把模型的回复也原样存回去,否则模型不知道它自己说过什么。
  • input("你:") 会阻塞等待用户输入;Ctrl+C 会抛 KeyboardInterrupt,需要捕获才能正常退出而非满屏报错。
  • /clear 的本质不是「删除模型记忆」,而是把 messages 重置为只剩 system 一条。
  • 每轮都全量重发 messages,上下文越长,费用与延迟越高;LLM_TIMEOUT 建议随对话长度适当调大。历史裁剪与会话保存属于进阶话题,后续章节再讲。

5. 控制回复:temperature 与 max_tokens

对话循环里也可以给每次请求加参数。第 2 章的 chat_text(messages, **kwargs) 会把多余参数透传给 SDK,常用的两个是:

from llm_client import LLMClient

llm = LLMClient()
answer = llm.chat_text(
    [{"role": "user", "content": "请用一句话介绍 Python"}],
    temperature=0.7,     # 0~2:越大越随机有创意,越小越稳定可复现
    max_tokens=100,      # 回复最大 token 数,防止一次生成过长
)
print(answer)
# 输出示例(随模型与时间变化):Python 是一门语法简洁、上手容易的编程语言……

temperature 适合创作类任务调大、事实类任务调小;max_tokens 是省钱利器——模型回复超长时会被截断,如何处理截断属于第 8 章「循环控制」的内容,这里先认识参数。 上下文无限增长也是开销问题:每轮都重发全部历史,聊久了 token 费直线上升。简单粗暴的缓解是只保留系统提示和最近几条消息:

messages = [messages[0]] + messages[-4:]   # 系统提示 + 最近 4 条(含刚追加的用户消息)

裁剪策略(按轮数还是按 token 数、重要信息如何保留)属于会话管理进阶,后续章节再展开。

6. 从对话循环到智能体

多轮对话循环只有「问-答」一种动作。真正的智能体多了一样东西:模型不只回复文字,还能提出「调用某个工具」的请求,程序执行工具后再把结果喂回去,如此反复直到模型给出最终答案。第 4 章先解决「让模型输出可被程序解析的结构化数据」,第 5~6 章再装上工具调用。 小结:多轮对话 = 维护 messages 列表 + 收集输入 → 请求 → 打印回复 → 追加上下文,配合 /exit 退出、/clear 重置即可。能记住上下文的聊天循环,是后面一切智能体循环的地基。

笔记加载中…